From d20003036179be5392a109c4d8771fcf8796fe28 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Tue, 2 Dec 2025 17:00:44 -0500 Subject: [PATCH] Refactor server.py: extract routes into modular route files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move routes from monolithic server.py (2716 lines) to dedicated route modules (788 lines remaining): - routes/main.py: Homepage, books browser, resources page - routes/misc.py: Search, interlinear landing, random-verse, verse-of-the-day - routes/timeline.py: Biblical timeline page and PDF - routes/about.py: Stats, cross-references index, about page - routes/reading_plans.py: Reading plans index, detail, PDF - routes/topics.py: Topics index and detail - routes/strongs.py: Strong's concordance search and entries This reduces server.py by 71% and improves code organization. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- kjvstudy_org/routes/__init__.py | 16 + kjvstudy_org/routes/about.py | 269 ++++ kjvstudy_org/routes/main.py | 624 ++++++++ kjvstudy_org/routes/misc.py | 225 +++ kjvstudy_org/routes/reading_plans.py | 185 +++ kjvstudy_org/routes/strongs.py | 171 +++ kjvstudy_org/routes/timeline.py | 105 ++ kjvstudy_org/routes/topics.py | 100 ++ kjvstudy_org/server.py | 2014 +------------------------- 9 files changed, 1738 insertions(+), 1971 deletions(-) create mode 100644 kjvstudy_org/routes/about.py create mode 100644 kjvstudy_org/routes/main.py create mode 100644 kjvstudy_org/routes/misc.py create mode 100644 kjvstudy_org/routes/reading_plans.py create mode 100644 kjvstudy_org/routes/strongs.py create mode 100644 kjvstudy_org/routes/timeline.py create mode 100644 kjvstudy_org/routes/topics.py diff --git a/kjvstudy_org/routes/__init__.py b/kjvstudy_org/routes/__init__.py index 9e05dbf..1482c01 100644 --- a/kjvstudy_org/routes/__init__.py +++ b/kjvstudy_org/routes/__init__.py @@ -8,6 +8,14 @@ from .study_guides import router as study_guides_router, init_templates as init_ from .commentary import router as commentary_router, init_templates as init_commentary_templates from .stories import router as stories_router, init_templates as init_stories_templates from .utility import router as utility_router +from .bible import router as bible_router, init_bible_templates, init_commentary_functions as init_bible_commentary +from .reading_plans import router as reading_plans_router, init_templates as init_reading_plans_templates +from .topics import router as topics_router, init_templates as init_topics_templates +from .strongs import router as strongs_router, init_templates as init_strongs_templates +from .timeline import router as timeline_router, init_templates as init_timeline_templates +from .about import router as about_router, init_templates as init_about_templates +from .main import router as main_router, init_templates as init_main_templates +from .misc import router as misc_router, init_templates as init_misc_templates, init_search_family_tree __all__ = [ 'api_router', 'init_api_templates', @@ -17,4 +25,12 @@ __all__ = [ 'commentary_router', 'init_commentary_templates', 'stories_router', 'init_stories_templates', 'utility_router', + 'bible_router', 'init_bible_templates', 'init_bible_commentary', + 'reading_plans_router', 'init_reading_plans_templates', + 'topics_router', 'init_topics_templates', + 'strongs_router', 'init_strongs_templates', + 'timeline_router', 'init_timeline_templates', + 'about_router', 'init_about_templates', + 'main_router', 'init_main_templates', + 'misc_router', 'init_misc_templates', 'init_search_family_tree', ] diff --git a/kjvstudy_org/routes/about.py b/kjvstudy_org/routes/about.py new file mode 100644 index 0000000..e6468de --- /dev/null +++ b/kjvstudy_org/routes/about.py @@ -0,0 +1,269 @@ +"""About routes - stats, cross-references index, and about page.""" +import json +import re +from collections import defaultdict +from pathlib import Path + +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +from ..kjv import bible +from ..utils.books import OT_BOOKS, NT_BOOKS + +router = APIRouter() +templates = None + + +def init_templates(t: Jinja2Templates): + """Initialize templates for about routes.""" + global templates + templates = t + + +# ============================================================================= +# Routes +# ============================================================================= + +@router.get("/about/stats", response_class=HTMLResponse) +async def stats(request: Request): + """Hidden statistics page - comprehensive site metrics""" + data_dir = Path(__file__).parent.parent / "data" + + # Bible statistics + total_verses = bible.get_verse_count() + total_books = len(bible.get_books()) + total_chapters = len(bible.get_chapters()) + + # Calculate words in Bible + total_words = sum(len(verse.text.split()) for verse in bible.iter_verses()) + + # Count unique book types + ot_books = len(OT_BOOKS) + nt_books = len(NT_BOOKS) + + # Data file statistics + total_json_files = len(list(data_dir.glob('**/*.json'))) + + # Verse commentary statistics + verse_commentary_files = len(list((data_dir / 'verse_commentary').glob('*.json'))) + total_commentary_verses = 0 + total_commentary_words = 0 + for file in (data_dir / 'verse_commentary').glob('*.json'): + data = json.load(open(file)) + commentary = data.get('commentary', {}) + for chapter in commentary.values(): + for verse_data in chapter.values(): + total_commentary_verses += 1 + # Count words in analysis + historical + analysis = verse_data.get('analysis', '') + historical = verse_data.get('historical', '') + # Strip HTML tags for accurate word count + clean_analysis = re.sub(r'<[^>]+>', '', analysis) + clean_historical = re.sub(r'<[^>]+>', '', historical) + total_commentary_words += len(clean_analysis.split()) + len(clean_historical.split()) + + # Cross-reference statistics + cross_reference_files = len(list((data_dir / 'cross_references').glob('*.json'))) + total_cross_refs = 0 + verses_with_cross_refs = 0 + for file in (data_dir / 'cross_references').glob('*.json'): + data = json.load(open(file)) + verses_with_cross_refs += len(data) + for verse_refs in data.values(): + total_cross_refs += len(verse_refs) + + # Red letter statistics + red_letter_data = json.load(open(data_dir / 'red_letter_verses.json')) + total_red_letter_verses = len(red_letter_data['verses']) + + # Study resources + study_guide_files = len(list((data_dir / 'study_guides').glob('*.json'))) + topic_files = len(list((data_dir / 'topics').glob('*.json'))) + resource_files = len(list((data_dir / 'resources').glob('*.json'))) + story_files = len(list((data_dir / 'stories').glob('*.json'))) + + # Interlinear data size + interlinear_file = data_dir / 'interlinear.json.gz' + interlinear_size_mb = interlinear_file.stat().st_size / 1024 / 1024 if interlinear_file.exists() else 0 + + # Calculate total data directory size + total_data_size = sum(f.stat().st_size for f in data_dir.glob('**/*') if f.is_file()) + total_data_size_mb = total_data_size / 1024 / 1024 + + # Book abbreviations + bible_metadata_file = data_dir / 'bible_metadata.json' + bible_metadata = json.load(open(bible_metadata_file)) + total_abbreviations = len(bible_metadata.get('book_abbreviations', {})) + + # Biographies + bio_data = json.load(open(data_dir / 'biographies.json')) + total_biographies = len(bio_data.get('biographies', {})) + + # Reading plans + reading_plan_files = len(list((data_dir / 'reading_plans').glob('*.json'))) + + # Strong's concordance + strongs_dir = data_dir / 'strongs' + if strongs_dir.exists(): + hebrew_data = json.load(open(strongs_dir / 'hebrew.json')) + greek_data = json.load(open(strongs_dir / 'greek.json')) + total_hebrew_entries = len(hebrew_data) + total_greek_entries = len(greek_data) + else: + total_hebrew_entries = 0 + total_greek_entries = 0 + + stats_data = { + 'bible': { + 'total_verses': total_verses, + 'total_books': total_books, + 'ot_books': ot_books, + 'nt_books': nt_books, + 'total_chapters': total_chapters, + 'total_words': total_words, + 'avg_words_per_verse': round(total_words / total_verses, 1), + 'avg_verses_per_chapter': round(total_verses / total_chapters, 1), + }, + 'commentary': { + 'files': verse_commentary_files, + 'verses_covered': total_commentary_verses, + 'total_words': total_commentary_words, + 'avg_words_per_verse': round(total_commentary_words / total_commentary_verses, 1) if total_commentary_verses > 0 else 0, + 'coverage_percent': round((total_commentary_verses / total_verses) * 100, 1), + }, + 'cross_references': { + 'files': cross_reference_files, + 'verses_with_refs': verses_with_cross_refs, + 'total_references': total_cross_refs, + 'avg_refs_per_verse': round(total_cross_refs / verses_with_cross_refs, 1) if verses_with_cross_refs > 0 else 0, + 'coverage_percent': round((verses_with_cross_refs / total_verses) * 100, 1), + }, + 'red_letter': { + 'total_verses': total_red_letter_verses, + 'percent_of_bible': round((total_red_letter_verses / total_verses) * 100, 1), + }, + 'study_resources': { + 'study_guides': study_guide_files, + 'topics': topic_files, + 'resources': resource_files, + 'stories': story_files, + 'biographies': total_biographies, + 'reading_plans': reading_plan_files, + }, + 'language_tools': { + 'hebrew_entries': total_hebrew_entries, + 'greek_entries': total_greek_entries, + 'total_strongs': total_hebrew_entries + total_greek_entries, + 'interlinear_size_mb': round(interlinear_size_mb, 1), + }, + 'data': { + 'total_json_files': total_json_files, + 'total_size_mb': round(total_data_size_mb, 1), + 'book_abbreviations': total_abbreviations, + } + } + + books = bible.get_books() + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "About", "url": "/about"}, + {"text": "Statistics", "url": None} + ] + + return templates.TemplateResponse( + "stats.html", + { + "request": request, + "books": books, + "stats": stats_data, + "breadcrumbs": breadcrumbs, + } + ) + + +@router.get("/about/cross-references", response_class=HTMLResponse) +async def cross_references_index(request: Request): + """Cross-references index - list all verses with cross-references""" + data_dir = Path(__file__).parent.parent / "data" / "cross_references" + + # Build index of all verses with cross-references, grouped by book + crossref_index = defaultdict(lambda: defaultdict(list)) + + for file in sorted(data_dir.glob('*.json')): + with open(file, 'r') as f: + data = json.load(f) + + for verse_key, refs in data.items(): + # Parse verse key: "Book:Chapter:Verse" + parts = verse_key.split(':') + if len(parts) == 3: + book, chapter, verse = parts + crossref_index[book][int(chapter)].append({ + 'verse': int(verse), + 'ref_count': len(refs) + }) + + # Sort books in biblical order (OT then NT) + biblical_order = OT_BOOKS + NT_BOOKS + book_order = {book: i for i, book in enumerate(biblical_order)} + + # Convert to regular dict and sort + crossref_index = { + book: { + chapter: sorted(verses, key=lambda x: x['verse']) + for chapter, verses in sorted(chapters.items()) + } + for book, chapters in sorted(crossref_index.items(), key=lambda x: book_order.get(x[0], 999)) + } + + # Calculate statistics + total_books = len(crossref_index) + total_verses = sum( + len(verses) + for chapters in crossref_index.values() + for verses in chapters.values() + ) + total_refs = sum( + sum(v['ref_count'] for v in verses) + for chapters in crossref_index.values() + for verses in chapters.values() + ) + + books = bible.get_books() + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "About", "url": "/about"}, + {"text": "Cross-References Index", "url": None} + ] + + return templates.TemplateResponse( + "cross_references_index.html", + { + "request": request, + "books": books, + "crossref_index": crossref_index, + "total_books": total_books, + "total_verses": total_verses, + "total_refs": total_refs, + "breadcrumbs": breadcrumbs, + } + ) + + +@router.get("/about", response_class=HTMLResponse) +async def about(request: Request): + """About page - site information, creator, data sources, theological approach""" + books = bible.get_books() + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "About", "url": None} + ] + return templates.TemplateResponse( + "about.html", + { + "request": request, + "books": books, + "breadcrumbs": breadcrumbs, + } + ) diff --git a/kjvstudy_org/routes/main.py b/kjvstudy_org/routes/main.py new file mode 100644 index 0000000..0dba5c3 --- /dev/null +++ b/kjvstudy_org/routes/main.py @@ -0,0 +1,624 @@ +"""Main page routes - homepage, books browser, and resources.""" +import hashlib +import re +from datetime import datetime + +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +from ..kjv import bible + +router = APIRouter() +templates = None + + +def init_templates(t: Jinja2Templates): + """Initialize templates for main routes.""" + global templates + templates = t + + +# ============================================================================= +# Helper Functions +# ============================================================================= + +def verse_reference_to_url(reference: str): + """Convert a verse reference to a URL path. + + Examples: + "John 3:16" -> "/book/John/chapter/3#verse-16" + "Romans 8:38-39" -> "/book/Romans/chapter/8#verse-38-39" + "Ephesians 2:8-9" -> "/book/Ephesians/chapter/2#verse-8-9" + """ + # Pattern: Book Chapter:Verse or Book Chapter:Verse-Verse + match = re.match(r'^(.+?)\s+(\d+):(\d+)(?:-(\d+))?$', reference.strip()) + if not match: + return None + + book = match.group(1).strip() + chapter = match.group(2) + verse_start = match.group(3) + verse_end = match.group(4) + + if verse_end: + # Verse range - link to chapter with anchor + return f"/book/{book}/chapter/{chapter}#verse-{verse_start}-{verse_end}" + else: + # Single verse - link to chapter with anchor + return f"/book/{book}/chapter/{chapter}#verse-{verse_start}" + + +def get_daily_verse(date_str=None): + """Get the verse of the day based on a specific date (or current date if not provided)""" + # Use date as seed for consistent daily verse + if date_str is None: + date_str = datetime.now().strftime("%Y-%m-%d") + seed = int(hashlib.md5(date_str.encode()).hexdigest(), 16) % 1000000 + + # Featured verses for rotation + featured_verses = [ + ("John", 3, 16), + ("Jeremiah", 29, 11), + ("Philippians", 4, 13), + ("Romans", 8, 28), + ("Proverbs", 3, 5), + ("Isaiah", 41, 10), + ("Matthew", 11, 28), + ("1 John", 4, 19), + ("Psalms", 23, 1), + ("2 Corinthians", 5, 17), + ("Ephesians", 2, 8), + ("Romans", 10, 9), + ("1 Peter", 5, 7), + ("James", 1, 5), + ("Philippians", 4, 19), + ("Psalms", 119, 105), + ("Matthew", 6, 33), + ("Romans", 12, 2), + ("1 Corinthians", 13, 13), + ("Galatians", 5, 22), + ("Hebrews", 11, 1), + ("1 Thessalonians", 5, 18), + ("Psalms", 46, 1), + ("Isaiah", 40, 31), + ("Matthew", 5, 16), + ("Romans", 15, 13), + ("Colossians", 3, 23), + ("1 John", 1, 9), + ("Psalms", 37, 4), + ("Proverbs", 27, 17) + ] + + # Select verse based on seed + verse_index = seed % len(featured_verses) + book, chapter, verse = featured_verses[verse_index] + + verse_text = bible.get_verse_text(book, chapter, verse) + if not verse_text: + # Fallback to John 3:16 + book, chapter, verse = "John", 3, 16 + verse_text = bible.get_verse_text(book, chapter, verse) + + return { + "book": book, + "chapter": chapter, + "verse": verse, + "text": verse_text, + "reference": f"{book} {chapter}:{verse}", + "date": date_str + } + + +# ============================================================================= +# Routes +# ============================================================================= + +@router.get("/", response_class=HTMLResponse) +async def read_root(request: Request): + books = bible.get_books() + daily_verse = get_daily_verse() + + # Define study guide categories + study_guides = { + "Foundational Studies": [ + { + "title": "New Believer's Guide", + "description": "Essential truths for new Christians", + "slug": "new-believer", + "verses": ["John 3:16", "Romans 10:9", "1 John 1:9", "2 Corinthians 5:17"] + }, + { + "title": "Salvation by Grace", + "description": "Understanding God's gift of salvation", + "slug": "salvation", + "verses": ["Ephesians 2:8-9", "Romans 3:23", "Romans 6:23", "Titus 3:5"] + }, + { + "title": "The Gospel Message", + "description": "The good news of Jesus Christ", + "slug": "gospel", + "verses": ["1 Corinthians 15:3-4", "Romans 1:16", "Mark 16:15", "Acts 4:12"] + } + ], + "Character & Living": [ + { + "title": "Fruits of the Spirit", + "description": "Developing Christian character", + "slug": "fruits-spirit", + "verses": ["Galatians 5:22-23", "1 Corinthians 13:4-7", "Philippians 4:8", "Colossians 3:12-14"] + }, + { + "title": "Prayer & Faith", + "description": "Growing in prayer and trust", + "slug": "prayer-faith", + "verses": ["Matthew 6:9-13", "1 Thessalonians 5:17", "Hebrews 11:1", "James 1:6"] + }, + { + "title": "Christian Living", + "description": "Walking as followers of Christ", + "slug": "christian-living", + "verses": ["Romans 12:1-2", "1 Peter 2:9", "Matthew 5:14-16", "Philippians 2:14-16"] + } + ], + "Biblical Themes": [ + { + "title": "God's Love", + "description": "Understanding the depth of God's love", + "slug": "gods-love", + "verses": ["1 John 4:8", "John 3:16", "Romans 8:38-39", "1 John 3:1"] + }, + { + "title": "Hope & Comfort", + "description": "Finding hope in difficult times", + "slug": "hope-comfort", + "verses": ["Romans 15:13", "2 Corinthians 1:3-4", "Psalm 23:4", "Isaiah 41:10"] + }, + { + "title": "Wisdom & Guidance", + "description": "Seeking God's wisdom for life", + "slug": "wisdom-guidance", + "verses": ["Proverbs 3:5-6", "James 1:5", "Psalm 119:105", "Proverbs 27:17"] + } + ], + "Doctrinal Studies": [ + { + "title": "The Trinity", + "description": "Understanding God as Father, Son, and Holy Spirit", + "slug": "trinity", + "verses": ["Matthew 28:19", "2 Corinthians 13:14", "1 Peter 1:2", "John 14:16-17"] + }, + { + "title": "The Resurrection", + "description": "Christ's victory over death and our hope", + "slug": "resurrection", + "verses": ["1 Corinthians 15:20-22", "Romans 6:4-5", "John 11:25-26", "1 Thessalonians 4:16-17"] + }, + { + "title": "Heaven & Eternity", + "description": "Our eternal home with God", + "slug": "heaven-eternity", + "verses": ["Revelation 21:1-4", "John 14:2-3", "Philippians 3:20-21", "1 Corinthians 2:9"] + } + ], + "Family & Relationships": [ + { + "title": "Biblical Marriage", + "description": "God's design for marriage", + "slug": "biblical-marriage", + "verses": ["Ephesians 5:22-33", "Genesis 2:24", "1 Corinthians 7:3-5", "Hebrews 13:4"] + }, + { + "title": "Raising Children", + "description": "Biblical principles for parenting", + "slug": "raising-children", + "verses": ["Proverbs 22:6", "Ephesians 6:4", "Deuteronomy 6:6-7", "Colossians 3:21"] + }, + { + "title": "Money & Stewardship", + "description": "Biblical wisdom on finances", + "slug": "money-stewardship", + "verses": ["Malachi 3:10", "Luke 16:10-11", "1 Timothy 6:10", "Proverbs 3:9-10"] + } + ] + } + + # Process verse references to add URLs + for category in study_guides.values(): + for guide in category: + guide['verse_refs'] = [ + { + 'text': verse, + 'url': verse_reference_to_url(verse) or '#' + } + for verse in guide['verses'] + ] + + return templates.TemplateResponse( + request, "index.html", {"books": books, "daily_verse": daily_verse, "study_guides": study_guides} + ) + + +@router.get("/books", response_class=HTMLResponse) +async def books_page(request: Request): + """Browse all books of the Bible""" + books = bible.get_books() + + # Define book categories with types + book_types = { + # Old Testament + 'Genesis': 'law', 'Exodus': 'law', 'Leviticus': 'law', 'Numbers': 'law', 'Deuteronomy': 'law', + 'Joshua': 'historical', 'Judges': 'historical', 'Ruth': 'historical', + '1 Samuel': 'historical', '2 Samuel': 'historical', '1 Kings': 'historical', '2 Kings': 'historical', + '1 Chronicles': 'historical', '2 Chronicles': 'historical', 'Ezra': 'historical', + 'Nehemiah': 'historical', 'Esther': 'historical', + 'Job': 'wisdom', 'Psalms': 'wisdom', 'Proverbs': 'wisdom', 'Ecclesiastes': 'wisdom', 'Song of Solomon': 'wisdom', + 'Isaiah': 'major-prophets', 'Jeremiah': 'major-prophets', 'Lamentations': 'major-prophets', + 'Ezekiel': 'major-prophets', 'Daniel': 'major-prophets', + 'Hosea': 'minor-prophets', 'Joel': 'minor-prophets', 'Amos': 'minor-prophets', + 'Obadiah': 'minor-prophets', 'Jonah': 'minor-prophets', 'Micah': 'minor-prophets', + 'Nahum': 'minor-prophets', 'Habakkuk': 'minor-prophets', 'Zephaniah': 'minor-prophets', + 'Haggai': 'minor-prophets', 'Zechariah': 'minor-prophets', 'Malachi': 'minor-prophets', + # New Testament + 'Matthew': 'gospels', 'Mark': 'gospels', 'Luke': 'gospels', 'John': 'gospels', + 'Acts': 'acts', + 'Romans': 'pauline', '1 Corinthians': 'pauline', '2 Corinthians': 'pauline', + 'Galatians': 'pauline', 'Ephesians': 'pauline', 'Philippians': 'pauline', 'Colossians': 'pauline', + '1 Thessalonians': 'pauline', '2 Thessalonians': 'pauline', + '1 Timothy': 'pauline', '2 Timothy': 'pauline', 'Titus': 'pauline', 'Philemon': 'pauline', + 'Hebrews': 'general', 'James': 'general', '1 Peter': 'general', '2 Peter': 'general', + '1 John': 'general', '2 John': 'general', '3 John': 'general', 'Jude': 'general', + 'Revelation': 'apocalyptic' + } + + # Organize books by testament + old_testament_books = [ + 'Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', 'Joshua', 'Judges', 'Ruth', + '1 Samuel', '2 Samuel', '1 Kings', '2 Kings', '1 Chronicles', '2 Chronicles', 'Ezra', + 'Nehemiah', 'Esther', 'Job', 'Psalms', 'Proverbs', 'Ecclesiastes', 'Song of Solomon', + 'Isaiah', 'Jeremiah', 'Lamentations', 'Ezekiel', 'Daniel', 'Hosea', 'Joel', 'Amos', + 'Obadiah', 'Jonah', 'Micah', 'Nahum', 'Habakkuk', 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi' + ] + + new_testament_books = [ + 'Matthew', 'Mark', 'Luke', 'John', 'Acts', 'Romans', '1 Corinthians', '2 Corinthians', + 'Galatians', 'Ephesians', 'Philippians', 'Colossians', '1 Thessalonians', '2 Thessalonians', + '1 Timothy', '2 Timothy', 'Titus', 'Philemon', 'Hebrews', 'James', '1 Peter', '2 Peter', + '1 John', '2 John', '3 John', 'Jude', 'Revelation' + ] + + # Get chapter counts for each book + def get_chapter_count(book_name): + chapters = bible.get_chapters_for_book(book_name) + return len(chapters) + + old_testament = [ + { + 'name': book, + 'chapters': get_chapter_count(book), + 'available': book in books, + 'type': book_types.get(book, '') + } + for book in old_testament_books + ] + + new_testament = [ + { + 'name': book, + 'chapters': get_chapter_count(book), + 'available': book in books, + 'type': book_types.get(book, '') + } + for book in new_testament_books + ] + + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Books", "url": None} + ] + + return templates.TemplateResponse( + request, + "books.html", + { + "old_testament": old_testament, + "new_testament": new_testament, + "books": books, + "breadcrumbs": breadcrumbs + } + ) + + +@router.get("/resources", response_class=HTMLResponse) +async def resources_page(request: Request): + """Browse all theological resources""" + books = bible.get_books() + + # Organize resources into categories + resources = { + "People": [ + { + "name": "Biblical Prophets", + "url": "/biblical-prophets", + "description": "Explore the prophetic ministry throughout Scripture, from Isaiah to Malachi", + "count": "9 prophets" + }, + { + "name": "The Twelve Apostles", + "url": "/the-twelve-apostles", + "description": "The twelve disciples chosen by Jesus to be witnesses of His ministry", + "count": "12 apostles" + }, + { + "name": "Women of the Bible", + "url": "/women-of-the-bible", + "description": "Notable women of Scripture and their significance in redemptive history", + "count": "12 women" + } + ], + "Theology": [ + { + "name": "Biblical Angels", + "url": "/biblical-angels", + "description": "Angelic beings mentioned in Scripture, including Michael, Gabriel, and the heavenly host", + "count": "12 entries" + }, + { + "name": "The Tetragrammaton", + "url": "/tetragrammaton", + "description": "The sacred four-letter name of God (YHWH) and its profound significance", + "count": "Deep dive" + }, + { + "name": "Names of God", + "url": "/names-of-god", + "description": "The revelation of God's names throughout Scripture and their meanings", + "count": "14 names" + }, + { + "name": "Parables of Jesus", + "url": "/parables", + "description": "The parables spoken by Christ to illustrate spiritual truths", + "count": "11 parables" + }, + { + "name": "Miracles of Jesus", + "url": "/miracles-of-jesus", + "description": "Signs and wonders manifesting divine authority over nature, disease, demons, and death", + "count": "35+ miracles" + }, + { + "name": "I Am Statements", + "url": "/i-am-statements", + "description": "The seven 'I Am' statements of Jesus in John's Gospel revealing His divine nature", + "count": "7 statements" + }, + { + "name": "The Beatitudes", + "url": "/beatitudes", + "description": "The blessings proclaimed by Jesus in the Sermon on the Mount", + "count": "8 beatitudes" + }, + { + "name": "Ten Commandments", + "url": "/ten-commandments", + "description": "The moral law given by God to Moses on Mount Sinai", + "count": "10 commandments" + }, + { + "name": "Armor of God", + "url": "/armor-of-god", + "description": "The spiritual equipment for warfare described in Ephesians 6", + "count": "7 pieces" + }, + { + "name": "Prayers of the Bible", + "url": "/prayers-of-the-bible", + "description": "Sacred prayers from the Psalms, Jesus, Paul, and the early church", + "count": "20+ prayers" + }, + { + "name": "Biblical Covenants", + "url": "/biblical-covenants", + "description": "Divine covenants established between God and His people", + "count": "7 covenants" + }, + { + "name": "Fruits of the Spirit", + "url": "/fruits-of-the-spirit", + "description": "The nine graces of Galatians 5:22-23 manifested in believers through the Holy Spirit", + "count": "9 fruits" + } + ], + "Systematic Theology": [ + { + "name": "The Trinity", + "url": "/trinity", + "description": "The mystery of God revealed as Father, Son, and Holy Spirit—three Persons, one God", + "count": "4 categories" + }, + { + "name": "Christology", + "url": "/christology", + "description": "The Person and work of Jesus Christ—His deity, humanity, and offices", + "count": "4 categories" + }, + { + "name": "Pneumatology", + "url": "/pneumatology", + "description": "The doctrine of the Holy Spirit—His Person, deity, and work in believers", + "count": "4 categories" + }, + { + "name": "Soteriology", + "url": "/soteriology", + "description": "The doctrine of salvation—from election to glorification", + "count": "5 categories" + }, + { + "name": "Ecclesiology", + "url": "/ecclesiology", + "description": "The doctrine of the Church—its nature, mission, and governance", + "count": "4 categories" + }, + { + "name": "Eschatology", + "url": "/eschatology", + "description": "The doctrine of last things—Christ's return, judgment, and eternal state", + "count": "5 categories" + }, + { + "name": "The Kingdom of God", + "url": "/kingdom-of-god", + "description": "God's sovereign reign inaugurated in Christ and consummated at His return", + "count": "5 categories" + }, + { + "name": "Types and Shadows", + "url": "/types-and-shadows", + "description": "Old Testament persons, events, and institutions that prefigure Christ", + "count": "5 categories" + }, + { + "name": "Messianic Prophecies", + "url": "/messianic-prophecies", + "description": "Old Testament prophecies fulfilled in Jesus Christ", + "count": "5 categories" + }, + { + "name": "The Blood in Scripture", + "url": "/blood-in-scripture", + "description": "The theology of blood, sacrifice, and redemption throughout Scripture", + "count": "5 categories" + }, + { + "name": "Names and Titles of Christ", + "url": "/names-of-christ", + "description": "The names and titles ascribed to Jesus revealing His Person and work", + "count": "5 categories" + }, + { + "name": "Spirits & Demons", + "url": "/spirits-and-demons", + "description": "Biblical demonology—Satan, evil spirits, Legion, and spiritual warfare", + "count": "7 categories" + }, + { + "name": "Personifications", + "url": "/personifications", + "description": "Abstract concepts given human form—Wisdom, Folly, Death, Sin, and more", + "count": "6 categories" + }, + { + "name": "Bibliology", + "url": "/bibliology", + "description": "The Doctrine of Scripture—inspiration, authority, sufficiency, and preservation", + "count": "4 categories" + }, + { + "name": "Theology Proper", + "url": "/theology-proper", + "description": "The Attributes of God—His incommunicable and communicable perfections", + "count": "4 categories" + }, + { + "name": "Anthropology", + "url": "/anthropology", + "description": "The Doctrine of Man—creation, constitution, and condition of humanity", + "count": "4 categories" + }, + { + "name": "Hamartiology", + "url": "/hamartiology", + "description": "The Doctrine of Sin—its origin, nature, transmission, and consequences", + "count": "4 categories" + }, + { + "name": "Providence", + "url": "/providence", + "description": "Divine Providence—God's preservation, governance, and concurrence in all things", + "count": "4 categories" + }, + { + "name": "Grace", + "url": "/grace", + "description": "The Doctrine of Grace—common grace, effectual grace, election, and perseverance", + "count": "4 categories" + }, + { + "name": "Justification", + "url": "/justification", + "description": "The Doctrine of Justification—declared righteous through faith in Christ alone", + "count": "4 categories" + }, + { + "name": "Sanctification", + "url": "/sanctification", + "description": "The Doctrine of Sanctification—progressive holiness through the Spirit", + "count": "4 categories" + }, + { + "name": "Law and Gospel", + "url": "/law-and-gospel", + "description": "The distinction between Law and Gospel—God's demands and His gracious provision", + "count": "4 categories" + }, + { + "name": "Worship", + "url": "/worship", + "description": "The Doctrine of Worship—regulative principle, elements, and the heart of worship", + "count": "4 categories" + } + ], + "History & Culture": [ + { + "name": "Biblical Festivals", + "url": "/biblical-festivals", + "description": "The appointed feasts and holy days ordained in the Law of Moses", + "count": "7 festivals" + }, + { + "name": "Biblical Geography", + "url": "/biblical-maps", + "description": "Locations mentioned in Scripture and their historical significance", + "count": "Maps & places", + "badge": "Interactive" + }, + { + "name": "Biblical Timeline", + "url": "/biblical-timeline", + "description": "Chronological overview of biblical events from Creation to Revelation", + "count": "Timeline" + }, + { + "name": "Genealogies", + "url": "/family-tree", + "description": "Interactive family tree from Adam to Jesus Christ with detailed person profiles", + "count": "160+ people", + "badge": "Interactive" + } + ], + "Study Tools": [ + { + "name": "Study Guides", + "url": "/study-guides", + "description": "In-depth guides for studying biblical books, themes, and doctrines", + "count": "Multiple guides" + } + ] + } + + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Resources", "url": None} + ] + + return templates.TemplateResponse( + request, + "resources.html", + { + "resources": resources, + "books": books, + "breadcrumbs": breadcrumbs + } + ) diff --git a/kjvstudy_org/routes/misc.py b/kjvstudy_org/routes/misc.py new file mode 100644 index 0000000..a3760d3 --- /dev/null +++ b/kjvstudy_org/routes/misc.py @@ -0,0 +1,225 @@ +"""Miscellaneous routes - search, interlinear, random verse, verse of the day.""" +import hashlib +import random +from datetime import datetime, timedelta + +from fastapi import APIRouter, Query, Request +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates + +from ..kjv import bible +from ..utils.search import perform_full_text_search + +router = APIRouter() +templates = None + +# Will be set by init_search_family_tree() +_search_family_tree_fn = None + + +def init_templates(t: Jinja2Templates): + """Initialize templates for misc routes.""" + global templates + templates = t + + +def init_search_family_tree(fn): + """Initialize the search_family_tree function from server.py.""" + global _search_family_tree_fn + _search_family_tree_fn = fn + + +# ============================================================================= +# Helper Functions +# ============================================================================= + +def get_daily_verse(date_str=None): + """Get the verse of the day based on a specific date (or current date if not provided)""" + # Use date as seed for consistent daily verse + if date_str is None: + date_str = datetime.now().strftime("%Y-%m-%d") + seed = int(hashlib.md5(date_str.encode()).hexdigest(), 16) % 1000000 + + # Featured verses for rotation + featured_verses = [ + ("John", 3, 16), + ("Jeremiah", 29, 11), + ("Philippians", 4, 13), + ("Romans", 8, 28), + ("Proverbs", 3, 5), + ("Isaiah", 41, 10), + ("Matthew", 11, 28), + ("1 John", 4, 19), + ("Psalms", 23, 1), + ("2 Corinthians", 5, 17), + ("Ephesians", 2, 8), + ("Romans", 10, 9), + ("1 Peter", 5, 7), + ("James", 1, 5), + ("Philippians", 4, 19), + ("Psalms", 119, 105), + ("Matthew", 6, 33), + ("Romans", 12, 2), + ("1 Corinthians", 13, 13), + ("Galatians", 5, 22), + ("Hebrews", 11, 1), + ("1 Thessalonians", 5, 18), + ("Psalms", 46, 1), + ("Isaiah", 40, 31), + ("Matthew", 5, 16), + ("Romans", 15, 13), + ("Colossians", 3, 23), + ("1 John", 1, 9), + ("Psalms", 37, 4), + ("Proverbs", 27, 17) + ] + + # Select verse based on seed + verse_index = seed % len(featured_verses) + book, chapter, verse = featured_verses[verse_index] + + verse_text = bible.get_verse_text(book, chapter, verse) + if not verse_text: + # Fallback to John 3:16 + book, chapter, verse = "John", 3, 16 + verse_text = bible.get_verse_text(book, chapter, verse) + + return { + "book": book, + "chapter": chapter, + "verse": verse, + "text": verse_text, + "reference": f"{book} {chapter}:{verse}", + "date": date_str + } + + +# ============================================================================= +# Routes +# ============================================================================= + +@router.get("/search", response_class=HTMLResponse) +async def search_page(request: Request, q: str = Query(None, description="Search query")): + """Search page with results (includes Bible verses and family tree)""" + books = bible.get_books() + search_results = [] + family_tree_results = [] + is_direct_verse = False + + if q and len(q.strip()) >= 2: + # Search Bible verses + search_results = perform_full_text_search(q.strip()) + # Check if this was a direct verse reference match + if search_results and len(search_results) == 1 and search_results[0].get("score") == 100.0: + is_direct_verse = True + + # Also search family tree (limit to 5 results) + if _search_family_tree_fn: + family_tree_results = _search_family_tree_fn(q.strip(), limit=5) + + return templates.TemplateResponse( + request, + "search.html", + { + "query": q or "", + "results": search_results, + "family_tree_results": family_tree_results, + "books": books, + "total_results": len(search_results) + len(family_tree_results), + "is_direct_verse": is_direct_verse + } + ) + + +@router.get("/interlinear", response_class=HTMLResponse) +async def interlinear_landing_page(request: Request): + """Landing page explaining interlinear Bible study""" + books = bible.get_books() + + # Featured verses with interlinear data + featured_verses = [ + {"reference": "John 3:16", "url": "/book/John/chapter/3/verse/16", "note": "God's love for the world"}, + {"reference": "Genesis 1:1", "url": "/book/Genesis/chapter/1/verse/1", "note": "In the beginning"}, + {"reference": "Psalm 23:1", "url": "/book/Psalms/chapter/23/verse/1", "note": "The Lord is my shepherd"}, + {"reference": "Romans 8:28", "url": "/book/Romans/chapter/8/verse/28", "note": "All things work together for good"}, + {"reference": "Matthew 28:19", "url": "/book/Matthew/chapter/28/verse/19", "note": "The Great Commission"}, + {"reference": "1 Corinthians 13:4", "url": "/book/1 Corinthians/chapter/13/verse/4", "note": "Love is patient"}, + ] + + # Build breadcrumbs + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Interlinear", "url": None} + ] + + return templates.TemplateResponse( + request, + "interlinear_landing.html", + { + "books": books, + "featured_verses": featured_verses, + "breadcrumbs": breadcrumbs + } + ) + + +@router.get("/random-verse") +async def random_verse(request: Request): + """Redirect to a random Bible verse""" + # Get all books + all_books = bible.get_books() + + # Pick a random book + book = random.choice(all_books) + + # Get all chapters for this book + chapters = bible.get_chapters_for_book(book) + + # Pick a random chapter + chapter = random.choice(chapters) + + # Get all verses for this chapter + verses = bible.get_verses_by_book_chapter(book, chapter) + + # Pick a random verse + verse = random.choice(verses) + + # Redirect to the verse page with cache control headers to ensure fresh random verse each time + response = RedirectResponse(url=f"/book/{book}/chapter/{chapter}/verse/{verse.verse}", status_code=302) + response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "0" + return response + + +@router.get("/verse-of-the-day", response_class=HTMLResponse) +async def verse_of_the_day_page(request: Request): + """Verse of the day page""" + books = bible.get_books() + daily_verse = get_daily_verse() + + # Generate past 30 days of verses + past_verses = [] + today = datetime.now() + for i in range(1, 31): # Past 30 days (not including today) + past_date = today - timedelta(days=i) + date_str = past_date.strftime("%Y-%m-%d") + verse = get_daily_verse(date_str) + past_verses.append(verse) + + # Build breadcrumbs + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Verse of the Day", "url": "/verse-of-the-day"} + ] + + return templates.TemplateResponse( + request, + "verse_of_the_day.html", + { + "books": books, + "daily_verse": daily_verse, + "past_verses": past_verses, + "breadcrumbs": breadcrumbs + } + ) diff --git a/kjvstudy_org/routes/reading_plans.py b/kjvstudy_org/routes/reading_plans.py new file mode 100644 index 0000000..3bcfb20 --- /dev/null +++ b/kjvstudy_org/routes/reading_plans.py @@ -0,0 +1,185 @@ +"""Reading plans routes - browse and view Bible reading plans.""" +import re + +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import HTMLResponse, StreamingResponse +from fastapi.templating import Jinja2Templates + +from ..kjv import bible +from ..reading_plans import get_plan, get_plan_summary +from ..utils.books import normalize_book_name, OT_BOOKS, NT_BOOKS +from ..utils.pdf import WEASYPRINT_AVAILABLE, render_html_to_pdf_async + +router = APIRouter() +templates = None + + +def init_templates(t: Jinja2Templates): + """Initialize templates for reading plans routes.""" + global templates + templates = t + + +# ============================================================================= +# Helper Functions +# ============================================================================= + +def parse_reading_reference(ref: str) -> list: + """Parse a reading reference like 'Genesis 1-3' or 'Matthew 1' into chapter list. + + Returns list of tuples: [(book, chapter), ...] + """ + # Handle patterns like "Genesis 1-3", "Matthew 1", "1 John 2-3" + # Pattern: optional number prefix + book name + chapter range + pattern = r'^((?:\d\s+)?[A-Za-z]+(?:\s+[A-Za-z]+)?)\s+(\d+)(?:-(\d+))?$' + match = re.match(pattern, ref.strip()) + if not match: + return [] + + book = match.group(1) + start_ch = int(match.group(2)) + end_ch = int(match.group(3)) if match.group(3) else start_ch + + # Normalize book name - if it's already canonical, use it as-is + normalized = normalize_book_name(book) + if not normalized: + # Check if it's already a valid canonical name + all_books = OT_BOOKS + NT_BOOKS + if book in all_books: + normalized = book + else: + return [] + + return [(normalized, ch) for ch in range(start_ch, end_ch + 1)] + + +def get_reading_text(readings: list) -> list: + """Get the Bible text for a list of reading references. + + Returns list of dicts with book, chapter, and verses. + """ + result = [] + for ref in readings: + chapters = parse_reading_reference(ref) + for book, chapter in chapters: + verses = bible.get_verses_by_book_chapter(book, chapter) + if verses: + result.append({ + 'book': book, + 'chapter': chapter, + 'verses': verses, + 'reference': f"{book} {chapter}" + }) + return result + + +# ============================================================================= +# Routes +# ============================================================================= + +@router.get("/reading-plans", response_class=HTMLResponse) +async def reading_plans_page(request: Request): + """Browse Bible reading plans""" + books = bible.get_books() + plans = get_plan_summary() + + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Reading Plans", "url": None} + ] + + return templates.TemplateResponse( + request, + "reading_plans.html", + { + "plans": plans, + "books": books, + "breadcrumbs": breadcrumbs + } + ) + + +@router.get("/reading-plans/{plan_id}", response_class=HTMLResponse) +async def reading_plan_detail(request: Request, plan_id: str): + """View a specific reading plan""" + books = bible.get_books() + plan = get_plan(plan_id) + + if not plan: + raise HTTPException(status_code=404, detail="Reading plan not found") + + # Pass day info without text - text will be lazy loaded via API + all_days = plan.get('days') or plan.get('sample_days', []) + days_data = [] + for day in all_days: + day_data = { + 'day': day['day'], + 'theme': day.get('theme', ''), + 'readings': day['readings'] + } + days_data.append(day_data) + + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Reading Plans", "url": "/reading-plans"}, + {"text": plan["name"], "url": None} + ] + + return templates.TemplateResponse( + request, + "reading_plan_detail.html", + { + "plan": plan, + "plan_id": plan_id, + "books": books, + "breadcrumbs": breadcrumbs, + "pdf_available": WEASYPRINT_AVAILABLE, + "pdf_url": f"/reading-plans/{plan_id}/pdf" if WEASYPRINT_AVAILABLE else None, + "days_data": days_data, + "total_days": plan.get('duration_days', len(days_data)) + } + ) + + +@router.get("/reading-plans/{plan_id}/pdf") +async def reading_plan_pdf(plan_id: str): + """Generate a PDF export for a reading plan.""" + if not WEASYPRINT_AVAILABLE: + raise HTTPException( + status_code=503, + detail="PDF generation is not available. WeasyPrint system libraries are not installed." + ) + + plan = get_plan(plan_id) + if not plan: + raise HTTPException(status_code=404, detail="Reading plan not found") + + # Include full Bible text for all plans (including 365-day plans) + include_text = True + + days_with_text = None + if include_text: + all_days = plan.get('days') or plan.get('sample_days', []) + days_with_text = [] + for day in all_days: + day_data = { + 'day': day['day'], + 'theme': day.get('theme', ''), + 'readings': day['readings'], + 'text': get_reading_text(day['readings']) + } + days_with_text.append(day_data) + + html_content = templates.get_template("reading_plan_pdf.html").render( + plan=plan, + include_text=include_text, + days_with_text=days_with_text + ) + pdf_buffer = await render_html_to_pdf_async(html_content) + + filename = f"reading-plan-{plan_id}.pdf" + return StreamingResponse( + pdf_buffer, + media_type="application/pdf", + headers={"Content-Disposition": f"attachment; filename={filename}"} + ) diff --git a/kjvstudy_org/routes/strongs.py b/kjvstudy_org/routes/strongs.py new file mode 100644 index 0000000..93895d0 --- /dev/null +++ b/kjvstudy_org/routes/strongs.py @@ -0,0 +1,171 @@ +"""Strong's Concordance routes - Hebrew and Greek word study.""" +import re + +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +from ..kjv import bible +from ..strongs import format_strongs_entry, search_strongs, get_all_strongs +from ..interlinear_loader import find_verses_by_strongs + +router = APIRouter() +templates = None + + +def init_templates(t: Jinja2Templates): + """Initialize templates for Strong's routes.""" + global templates + templates = t + + +# ============================================================================= +# Routes +# ============================================================================= + +@router.get("/strongs", response_class=HTMLResponse) +async def strongs_index(request: Request, q: str = None): + """Strong's Concordance search and lookup page.""" + results = [] + if q: + results = search_strongs(q, language="both", limit=100) + + books = bible.get_books() + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Strong's Concordance", "url": None} + ] + + return templates.TemplateResponse( + request, + "strongs_index.html", + { + "query": q or "", + "results": results, + "books": books, + "breadcrumbs": breadcrumbs + } + ) + + +@router.get("/strongs/hebrew", response_class=HTMLResponse) +async def strongs_hebrew_index(request: Request, page: int = 1): + """Paginated index of all Hebrew Strong's entries.""" + data = get_all_strongs("hebrew", page=page, per_page=100) + + books = bible.get_books() + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Strong's Concordance", "url": "/strongs"}, + {"text": "Hebrew", "url": None} + ] + + return templates.TemplateResponse( + request, + "strongs_language_index.html", + { + "language": "Hebrew", + "language_code": "hebrew", + "entries": data["entries"], + "page": data["page"], + "total_pages": data["total_pages"], + "total": data["total"], + "books": books, + "breadcrumbs": breadcrumbs + } + ) + + +@router.get("/strongs/greek", response_class=HTMLResponse) +async def strongs_greek_index(request: Request, page: int = 1): + """Paginated index of all Greek Strong's entries.""" + data = get_all_strongs("greek", page=page, per_page=100) + + books = bible.get_books() + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Strong's Concordance", "url": "/strongs"}, + {"text": "Greek", "url": None} + ] + + return templates.TemplateResponse( + request, + "strongs_language_index.html", + { + "language": "Greek", + "language_code": "greek", + "entries": data["entries"], + "page": data["page"], + "total_pages": data["total_pages"], + "total": data["total"], + "books": books, + "breadcrumbs": breadcrumbs + } + ) + + +@router.get("/strongs/{strongs_number}", response_class=HTMLResponse) +async def strongs_entry(request: Request, strongs_number: str): + """View a single Strong's concordance entry.""" + entry = format_strongs_entry(strongs_number) + + if not entry: + raise HTTPException( + status_code=404, + detail=f"Strong's number '{strongs_number}' not found" + ) + + # Find all verses containing this Strong's number + verse_occurrences = find_verses_by_strongs(strongs_number, limit=10000) + total_occurrences = len(verse_occurrences) + + # Fetch full verse text for each occurrence and highlight the word + for occ in verse_occurrences: + verse_text = bible.get_verse_text(occ["book"], occ["chapter"], occ["verse"]) + if verse_text: + # Highlight the English word in the verse text + english_word = occ.get("english", "") + if english_word and english_word in verse_text: + occ["verse_text"] = verse_text.replace( + english_word, + f'{english_word}', + 1 # Only highlight first occurrence + ) + else: + occ["verse_text"] = verse_text + else: + occ["verse_text"] = "" + + # Extract and fetch related Strong's entries from derivation + related_entries = [] + if entry.get("derivation"): + # Find all Strong's references like H1234 or G5678 + strongs_refs = re.findall(r'([HG])(\d+)', entry["derivation"]) + seen = set() + for prefix, num in strongs_refs: + ref = f"{prefix}{num}" + if ref.upper() != strongs_number.upper() and ref not in seen: + seen.add(ref) + related = format_strongs_entry(ref) + if related: + related_entries.append(related) + + books = bible.get_books() + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Strong's Concordance", "url": "/strongs"}, + {"text": strongs_number.upper(), "url": None} + ] + + return templates.TemplateResponse( + request, + "strongs_entry.html", + { + "entry": entry, + "books": books, + "breadcrumbs": breadcrumbs, + "verse_occurrences": verse_occurrences, + "total_occurrences": total_occurrences, + "related_entries": related_entries + } + ) diff --git a/kjvstudy_org/routes/timeline.py b/kjvstudy_org/routes/timeline.py new file mode 100644 index 0000000..2c812a2 --- /dev/null +++ b/kjvstudy_org/routes/timeline.py @@ -0,0 +1,105 @@ +"""Biblical timeline routes.""" +import json +from pathlib import Path + +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import HTMLResponse, StreamingResponse +from fastapi.templating import Jinja2Templates + +from ..kjv import bible +from ..utils.pdf import WEASYPRINT_AVAILABLE, render_html_to_pdf_async + +router = APIRouter() +templates = None + + +def init_templates(t: Jinja2Templates): + """Initialize templates for timeline routes.""" + global templates + templates = t + + +# ============================================================================= +# Helper Functions +# ============================================================================= + +def get_biblical_timeline_context(): + """ + Load comprehensive biblical timeline data from JSON file. + + Returns tuple of (timeline_events, introduction, chronology_note, chronology_comparison, conclusion) + """ + # Load timeline data from JSON file + data_dir = Path(__file__).parent.parent / "data" + timeline_path = data_dir / "biblical_timeline.json" + + with open(timeline_path, 'r', encoding='utf-8') as f: + timeline_data = json.load(f) + + timeline_events = timeline_data.get("timeline_events", {}) + introduction = timeline_data.get("introduction", "") + chronology_note = timeline_data.get("chronology_note", "") + chronology_comparison = timeline_data.get("chronology_comparison", []) + conclusion = timeline_data.get("conclusion", "") + + return timeline_events, introduction, chronology_note, chronology_comparison, conclusion + + +# ============================================================================= +# Routes +# ============================================================================= + +@router.get("/biblical-timeline", response_class=HTMLResponse) +def biblical_timeline_page(request: Request): + """Biblical timeline page showing major biblical events chronologically""" + books = bible.get_books() + + timeline_events, introduction, chronology_note, chronology_comparison, conclusion = get_biblical_timeline_context() + + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Resources", "url": "/resources"}, + {"text": "Biblical Timeline", "url": None} + ] + + return templates.TemplateResponse( + request, + "biblical_timeline.html", + { + "books": books, + "timeline_events": timeline_events, + "introduction": introduction, + "chronology_note": chronology_note, + "chronology_comparison": chronology_comparison, + "conclusion": conclusion, + "breadcrumbs": breadcrumbs, + "pdf_available": WEASYPRINT_AVAILABLE + } + ) + + +@router.get("/biblical-timeline/pdf") +async def biblical_timeline_pdf(): + """Generate PDF export for the biblical timeline.""" + if not WEASYPRINT_AVAILABLE: + raise HTTPException( + status_code=503, + detail="PDF generation is not available. WeasyPrint system libraries are not installed." + ) + + timeline_events, introduction, chronology_note, chronology_comparison, conclusion = get_biblical_timeline_context() + + html_content = templates.get_template("biblical_timeline_pdf.html").render( + timeline_events=timeline_events, + introduction=introduction, + chronology_note=chronology_note, + chronology_comparison=chronology_comparison, + conclusion=conclusion + ) + pdf_buffer = await render_html_to_pdf_async(html_content) + + return StreamingResponse( + pdf_buffer, + media_type="application/pdf", + headers={"Content-Disposition": "attachment; filename=biblical-timeline.pdf"} + ) diff --git a/kjvstudy_org/routes/topics.py b/kjvstudy_org/routes/topics.py new file mode 100644 index 0000000..5be7b5f --- /dev/null +++ b/kjvstudy_org/routes/topics.py @@ -0,0 +1,100 @@ +"""Topics routes - browse and view topical Bible studies.""" +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import HTMLResponse, StreamingResponse +from fastapi.templating import Jinja2Templates + +from ..kjv import bible +from ..topics import get_all_topics, get_topic_with_text +from ..utils.pdf import WEASYPRINT_AVAILABLE, render_html_to_pdf_async + +router = APIRouter() +templates = None + + +def init_templates(t: Jinja2Templates): + """Initialize templates for topics routes.""" + global templates + templates = t + + +# ============================================================================= +# Routes +# ============================================================================= + +@router.get("/topics", response_class=HTMLResponse) +async def topics_page(request: Request): + """Browse topical index of Bible themes""" + books = bible.get_books() + topics = get_all_topics() + + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Topics", "url": None} + ] + + return templates.TemplateResponse( + request, + "topics.html", + { + "topics": topics, + "books": books, + "breadcrumbs": breadcrumbs, + "pdf_available": WEASYPRINT_AVAILABLE + } + ) + + +@router.get("/topics/{topic_name}", response_class=HTMLResponse) +async def topic_detail(request: Request, topic_name: str): + """View verses for a specific topic""" + books = bible.get_books() + topic = get_topic_with_text(topic_name) + + if not topic: + raise HTTPException(status_code=404, detail="Topic not found") + + breadcrumbs = [ + {"text": "Home", "url": "/"}, + {"text": "Topics", "url": "/topics"}, + {"text": topic_name, "url": None} + ] + + return templates.TemplateResponse( + request, + "topic_detail.html", + { + "topic": topic, + "topic_name": topic_name, + "books": books, + "breadcrumbs": breadcrumbs, + "pdf_available": WEASYPRINT_AVAILABLE, + "pdf_url": f"/topics/{topic_name}/pdf" if WEASYPRINT_AVAILABLE else None + } + ) + + +@router.get("/topics/{topic_name}/pdf") +async def topic_detail_pdf(topic_name: str): + """Generate a PDF export for a topic detail page.""" + if not WEASYPRINT_AVAILABLE: + raise HTTPException( + status_code=503, + detail="PDF generation is not available. WeasyPrint system libraries are not installed." + ) + + topic = get_topic_with_text(topic_name) + if not topic: + raise HTTPException(status_code=404, detail="Topic not found") + + html_content = templates.get_template("topic_pdf.html").render( + topic=topic, + topic_name=topic_name, + ) + pdf_buffer = await render_html_to_pdf_async(html_content) + + filename = f"{topic_name}.pdf" + return StreamingResponse( + pdf_buffer, + media_type="application/pdf", + headers={"Content-Disposition": f"attachment; filename={filename}"} + ) diff --git a/kjvstudy_org/server.py b/kjvstudy_org/server.py index 3fc0a1f..6918455 100644 --- a/kjvstudy_org/server.py +++ b/kjvstudy_org/server.py @@ -35,6 +35,14 @@ from .routes import ( commentary_router, init_commentary_templates, stories_router, init_stories_templates, utility_router, + bible_router, init_bible_templates, init_bible_commentary, + reading_plans_router, init_reading_plans_templates, + topics_router, init_topics_templates, + strongs_router, init_strongs_templates, + timeline_router, init_timeline_templates, + about_router, init_about_templates, + main_router, init_main_templates, + misc_router, init_misc_templates, init_search_family_tree, ) from .routes.commentary import ( generate_commentary, @@ -108,6 +116,30 @@ app.include_router(stories_router) # Include the utility router (sitemap, robots.txt, health) app.include_router(utility_router) +# Include the Bible router (book, chapter, verse, interlinear routes) +app.include_router(bible_router) + +# Include the reading plans router +app.include_router(reading_plans_router) + +# Include the topics router +app.include_router(topics_router) + +# Include the Strong's Concordance router +app.include_router(strongs_router) + +# Include the timeline router +app.include_router(timeline_router) + +# Include the about router +app.include_router(about_router) + +# Include the main router (homepage, books, resources) +app.include_router(main_router) + +# Include the misc router (search, interlinear, random-verse, verse-of-the-day) +app.include_router(misc_router) + # Custom OpenAPI schema to only include /api routes def custom_openapi(): @@ -236,6 +268,15 @@ init_family_tree_templates(templates) init_study_guides_templates(templates) init_commentary_templates(templates) init_stories_templates(templates) +init_bible_templates(templates) +init_bible_commentary(generate_commentary, generate_chapter_overview, generate_book_commentary, generate_word_study_sidenotes) +init_reading_plans_templates(templates) +init_topics_templates(templates) +init_strongs_templates(templates) +init_timeline_templates(templates) +init_about_templates(templates) +init_main_templates(templates) +init_misc_templates(templates) # Load Scofield commentary for cross-references scofield_commentary = {} @@ -267,157 +308,6 @@ async def custom_http_exception_handler(request: Request, exc: StarletteHTTPExce return await http_exception_handler(request, exc) -@app.get("/search", response_class=HTMLResponse) -async def search_page(request: Request, q: str = Query(None, description="Search query")): - """Search page with results (includes Bible verses and family tree)""" - books = bible.get_books() - search_results = [] - family_tree_results = [] - is_direct_verse = False - - if q and len(q.strip()) >= 2: - # Search Bible verses - search_results = perform_full_text_search(q.strip()) - # Check if this was a direct verse reference match - if search_results and len(search_results) == 1 and search_results[0].get("score") == 100.0: - is_direct_verse = True - - # Also search family tree (limit to 5 results) - family_tree_results = search_family_tree(q.strip(), limit=5) - - return templates.TemplateResponse( - request, - "search.html", - { - "query": q or "", - "results": search_results, - "family_tree_results": family_tree_results, - "books": books, - "total_results": len(search_results) + len(family_tree_results), - "is_direct_verse": is_direct_verse - } - ) - -@app.get("/interlinear", response_class=HTMLResponse) -async def interlinear_landing_page(request: Request): - """Landing page explaining interlinear Bible study""" - books = bible.get_books() - - # Featured verses with interlinear data - featured_verses = [ - {"reference": "John 3:16", "url": "/book/John/chapter/3/verse/16", "note": "God's love for the world"}, - {"reference": "Genesis 1:1", "url": "/book/Genesis/chapter/1/verse/1", "note": "In the beginning"}, - {"reference": "Psalm 23:1", "url": "/book/Psalms/chapter/23/verse/1", "note": "The Lord is my shepherd"}, - {"reference": "Romans 8:28", "url": "/book/Romans/chapter/8/verse/28", "note": "All things work together for good"}, - {"reference": "Matthew 28:19", "url": "/book/Matthew/chapter/28/verse/19", "note": "The Great Commission"}, - {"reference": "1 Corinthians 13:4", "url": "/book/1 Corinthians/chapter/13/verse/4", "note": "Love is patient"}, - ] - - # Build breadcrumbs - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Interlinear", "url": None} - ] - - return templates.TemplateResponse( - request, - "interlinear_landing.html", - { - "books": books, - "featured_verses": featured_verses, - "breadcrumbs": breadcrumbs - } - ) - - -def verse_reference_to_url(reference: str): - """Convert a verse reference to a URL path. - - Examples: - "John 3:16" -> "/book/John/chapter/3#verse-16" - "Romans 8:38-39" -> "/book/Romans/chapter/8#verse-38-39" - "Ephesians 2:8-9" -> "/book/Ephesians/chapter/2#verse-8-9" - """ - # Pattern: Book Chapter:Verse or Book Chapter:Verse-Verse - match = re.match(r'^(.+?)\s+(\d+):(\d+)(?:-(\d+))?$', reference.strip()) - if not match: - return None - - book = match.group(1).strip() - chapter = match.group(2) - verse_start = match.group(3) - verse_end = match.group(4) - - if verse_end: - # Verse range - link to chapter with anchor - return f"/book/{book}/chapter/{chapter}#verse-{verse_start}-{verse_end}" - else: - # Single verse - link to chapter with anchor - return f"/book/{book}/chapter/{chapter}#verse-{verse_start}" - -@app.get("/random-verse") -async def random_verse(request: Request): - """Redirect to a random Bible verse""" - # Get all books - all_books = bible.get_books() - - # Pick a random book - book = random.choice(all_books) - - # Get all chapters for this book - chapters = bible.get_chapters_for_book(book) - - # Pick a random chapter - chapter = random.choice(chapters) - - # Get all verses for this chapter - verses = bible.get_verses_by_book_chapter(book, chapter) - - # Pick a random verse - verse = random.choice(verses) - - # Redirect to the verse page with cache control headers to ensure fresh random verse each time - response = RedirectResponse(url=f"/book/{book}/chapter/{chapter}/verse/{verse.verse}", status_code=302) - response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" - response.headers["Pragma"] = "no-cache" - response.headers["Expires"] = "0" - return response - - -@app.get("/verse-of-the-day", response_class=HTMLResponse) -async def verse_of_the_day_page(request: Request): - """Verse of the day page""" - books = bible.get_books() - daily_verse = get_daily_verse() - - # Generate past 30 days of verses - past_verses = [] - today = datetime.now() - for i in range(1, 31): # Past 30 days (not including today) - past_date = today - timedelta(days=i) - date_str = past_date.strftime("%Y-%m-%d") - verse = get_daily_verse(date_str) - past_verses.append(verse) - - # Build breadcrumbs - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Verse of the Day", "url": "/verse-of-the-day"} - ] - - return templates.TemplateResponse( - request, - "verse_of_the_day.html", - { - "books": books, - "daily_verse": daily_verse, - "past_verses": past_verses, - "breadcrumbs": breadcrumbs - } - ) - -# Note: API routes have been moved to routes/api.py and are included via app.include_router(api_router) - def expand_book_abbreviation(abbrev): """Expand common Bible book abbreviations to full names""" @@ -793,84 +683,6 @@ def search_family_tree(query: str, limit: Optional[int] = None) -> List[Dict]: return results -def get_biblical_timeline_context(): - """ - Load comprehensive biblical timeline data from JSON file. - - Returns tuple of (timeline_events, introduction, chronology_note, chronology_comparison, conclusion) - """ - # Load timeline data from JSON file - data_dir = PathLib(__file__).parent / "data" - timeline_path = data_dir / "biblical_timeline.json" - - with open(timeline_path, 'r', encoding='utf-8') as f: - timeline_data = json.load(f) - - timeline_events = timeline_data.get("timeline_events", {}) - introduction = timeline_data.get("introduction", "") - chronology_note = timeline_data.get("chronology_note", "") - chronology_comparison = timeline_data.get("chronology_comparison", []) - conclusion = timeline_data.get("conclusion", "") - - return timeline_events, introduction, chronology_note, chronology_comparison, conclusion - - -@app.get("/biblical-timeline", response_class=HTMLResponse) -def biblical_timeline_page(request: Request): - """Biblical timeline page showing major biblical events chronologically""" - books = bible.get_books() - - timeline_events, introduction, chronology_note, chronology_comparison, conclusion = get_biblical_timeline_context() - - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Resources", "url": "/resources"}, - {"text": "Biblical Timeline", "url": None} - ] - - return templates.TemplateResponse( - request, - "biblical_timeline.html", - { - "books": books, - "timeline_events": timeline_events, - "introduction": introduction, - "chronology_note": chronology_note, - "chronology_comparison": chronology_comparison, - "conclusion": conclusion, - "breadcrumbs": breadcrumbs, - "pdf_available": WEASYPRINT_AVAILABLE - } - ) - - -@app.get("/biblical-timeline/pdf") -async def biblical_timeline_pdf(): - """Generate PDF export for the biblical timeline.""" - if not WEASYPRINT_AVAILABLE: - raise HTTPException( - status_code=503, - detail="PDF generation is not available. WeasyPrint system libraries are not installed." - ) - - timeline_events, introduction, chronology_note, chronology_comparison, conclusion = get_biblical_timeline_context() - - html_content = templates.get_template("biblical_timeline_pdf.html").render( - timeline_events=timeline_events, - introduction=introduction, - chronology_note=chronology_note, - chronology_comparison=chronology_comparison, - conclusion=conclusion - ) - pdf_buffer = await render_html_to_pdf_async(html_content) - - return StreamingResponse( - pdf_buffer, - media_type="application/pdf", - headers={"Content-Disposition": "attachment; filename=biblical-timeline.pdf"} - ) - - def get_biblical_verses(name): """Get relevant Bible verses for a person based on their name""" verse_map = { @@ -972,1745 +784,5 @@ def get_daily_verse(date_str=None): } - -@app.get("/about/stats", response_class=HTMLResponse) -async def stats(request: Request): - """Hidden statistics page - comprehensive site metrics""" - import json - from pathlib import Path - - data_dir = Path(__file__).parent / "data" - - # Bible statistics - total_verses = bible.get_verse_count() - total_books = len(bible.get_books()) - total_chapters = len(bible.get_chapters()) - - # Calculate words in Bible - total_words = sum(len(verse.text.split()) for verse in bible.iter_verses()) - - # Count unique book types - ot_books = len(OT_BOOKS) - nt_books = len(NT_BOOKS) - - # Data file statistics - total_json_files = len(list(data_dir.glob('**/*.json'))) - - # Verse commentary statistics - verse_commentary_files = len(list((data_dir / 'verse_commentary').glob('*.json'))) - total_commentary_verses = 0 - total_commentary_words = 0 - for file in (data_dir / 'verse_commentary').glob('*.json'): - data = json.load(open(file)) - commentary = data.get('commentary', {}) - for chapter in commentary.values(): - for verse_data in chapter.values(): - total_commentary_verses += 1 - # Count words in analysis + historical - analysis = verse_data.get('analysis', '') - historical = verse_data.get('historical', '') - # Strip HTML tags for accurate word count - import re - clean_analysis = re.sub(r'<[^>]+>', '', analysis) - clean_historical = re.sub(r'<[^>]+>', '', historical) - total_commentary_words += len(clean_analysis.split()) + len(clean_historical.split()) - - # Cross-reference statistics - cross_reference_files = len(list((data_dir / 'cross_references').glob('*.json'))) - total_cross_refs = 0 - verses_with_cross_refs = 0 - for file in (data_dir / 'cross_references').glob('*.json'): - data = json.load(open(file)) - verses_with_cross_refs += len(data) - for verse_refs in data.values(): - total_cross_refs += len(verse_refs) - - # Red letter statistics - red_letter_data = json.load(open(data_dir / 'red_letter_verses.json')) - total_red_letter_verses = len(red_letter_data['verses']) - - # Study resources - study_guide_files = len(list((data_dir / 'study_guides').glob('*.json'))) - topic_files = len(list((data_dir / 'topics').glob('*.json'))) - resource_files = len(list((data_dir / 'resources').glob('*.json'))) - story_files = len(list((data_dir / 'stories').glob('*.json'))) - - # Interlinear data size - interlinear_file = data_dir / 'interlinear.json.gz' - interlinear_size_mb = interlinear_file.stat().st_size / 1024 / 1024 if interlinear_file.exists() else 0 - - # Calculate total data directory size - total_data_size = sum(f.stat().st_size for f in data_dir.glob('**/*') if f.is_file()) - total_data_size_mb = total_data_size / 1024 / 1024 - - # Book abbreviations - bible_metadata_file = data_dir / 'bible_metadata.json' - bible_metadata = json.load(open(bible_metadata_file)) - total_abbreviations = len(bible_metadata.get('book_abbreviations', {})) - - # Biographies - bio_data = json.load(open(data_dir / 'biographies.json')) - total_biographies = len(bio_data.get('biographies', {})) - - # Reading plans - reading_plan_files = len(list((data_dir / 'reading_plans').glob('*.json'))) - - # Strong's concordance - strongs_dir = data_dir / 'strongs' - if strongs_dir.exists(): - hebrew_data = json.load(open(strongs_dir / 'hebrew.json')) - greek_data = json.load(open(strongs_dir / 'greek.json')) - total_hebrew_entries = len(hebrew_data) - total_greek_entries = len(greek_data) - else: - total_hebrew_entries = 0 - total_greek_entries = 0 - - stats_data = { - 'bible': { - 'total_verses': total_verses, - 'total_books': total_books, - 'ot_books': ot_books, - 'nt_books': nt_books, - 'total_chapters': total_chapters, - 'total_words': total_words, - 'avg_words_per_verse': round(total_words / total_verses, 1), - 'avg_verses_per_chapter': round(total_verses / total_chapters, 1), - }, - 'commentary': { - 'files': verse_commentary_files, - 'verses_covered': total_commentary_verses, - 'total_words': total_commentary_words, - 'avg_words_per_verse': round(total_commentary_words / total_commentary_verses, 1) if total_commentary_verses > 0 else 0, - 'coverage_percent': round((total_commentary_verses / total_verses) * 100, 1), - }, - 'cross_references': { - 'files': cross_reference_files, - 'verses_with_refs': verses_with_cross_refs, - 'total_references': total_cross_refs, - 'avg_refs_per_verse': round(total_cross_refs / verses_with_cross_refs, 1) if verses_with_cross_refs > 0 else 0, - 'coverage_percent': round((verses_with_cross_refs / total_verses) * 100, 1), - }, - 'red_letter': { - 'total_verses': total_red_letter_verses, - 'percent_of_bible': round((total_red_letter_verses / total_verses) * 100, 1), - }, - 'study_resources': { - 'study_guides': study_guide_files, - 'topics': topic_files, - 'resources': resource_files, - 'stories': story_files, - 'biographies': total_biographies, - 'reading_plans': reading_plan_files, - }, - 'language_tools': { - 'hebrew_entries': total_hebrew_entries, - 'greek_entries': total_greek_entries, - 'total_strongs': total_hebrew_entries + total_greek_entries, - 'interlinear_size_mb': round(interlinear_size_mb, 1), - }, - 'data': { - 'total_json_files': total_json_files, - 'total_size_mb': round(total_data_size_mb, 1), - 'book_abbreviations': total_abbreviations, - } - } - - books = bible.get_books() - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "About", "url": "/about"}, - {"text": "Statistics", "url": None} - ] - - return templates.TemplateResponse( - "stats.html", - { - "request": request, - "books": books, - "stats": stats_data, - "breadcrumbs": breadcrumbs, - } - ) - - -@app.get("/about/cross-references", response_class=HTMLResponse) -async def cross_references_index(request: Request): - """Cross-references index - list all verses with cross-references""" - from collections import defaultdict - import json - from pathlib import Path - - data_dir = PathLib(__file__).parent / "data" / "cross_references" - - # Build index of all verses with cross-references, grouped by book - crossref_index = defaultdict(lambda: defaultdict(list)) - - for file in sorted(data_dir.glob('*.json')): - with open(file, 'r') as f: - data = json.load(f) - - for verse_key, refs in data.items(): - # Parse verse key: "Book:Chapter:Verse" - parts = verse_key.split(':') - if len(parts) == 3: - book, chapter, verse = parts - crossref_index[book][int(chapter)].append({ - 'verse': int(verse), - 'ref_count': len(refs) - }) - - # Sort books in biblical order (OT then NT) - biblical_order = OT_BOOKS + NT_BOOKS - book_order = {book: i for i, book in enumerate(biblical_order)} - - # Convert to regular dict and sort - crossref_index = { - book: { - chapter: sorted(verses, key=lambda x: x['verse']) - for chapter, verses in sorted(chapters.items()) - } - for book, chapters in sorted(crossref_index.items(), key=lambda x: book_order.get(x[0], 999)) - } - - # Calculate statistics - total_books = len(crossref_index) - total_verses = sum( - len(verses) - for chapters in crossref_index.values() - for verses in chapters.values() - ) - total_refs = sum( - sum(v['ref_count'] for v in verses) - for chapters in crossref_index.values() - for verses in chapters.values() - ) - - books = bible.get_books() - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "About", "url": "/about"}, - {"text": "Cross-References Index", "url": None} - ] - - return templates.TemplateResponse( - "cross_references_index.html", - { - "request": request, - "books": books, - "crossref_index": crossref_index, - "total_books": total_books, - "total_verses": total_verses, - "total_refs": total_refs, - "breadcrumbs": breadcrumbs, - } - ) - - -@app.get("/about", response_class=HTMLResponse) -async def about(request: Request): - """About page - site information, creator, data sources, theological approach""" - books = bible.get_books() - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "About", "url": None} - ] - return templates.TemplateResponse( - "about.html", - { - "request": request, - "books": books, - "breadcrumbs": breadcrumbs, - } - ) - - -@app.get("/", response_class=HTMLResponse) -async def read_root(request: Request): - books = bible.get_books() - daily_verse = get_daily_verse() - - # Define study guide categories - study_guides = { - "Foundational Studies": [ - { - "title": "New Believer's Guide", - "description": "Essential truths for new Christians", - "slug": "new-believer", - "verses": ["John 3:16", "Romans 10:9", "1 John 1:9", "2 Corinthians 5:17"] - }, - { - "title": "Salvation by Grace", - "description": "Understanding God's gift of salvation", - "slug": "salvation", - "verses": ["Ephesians 2:8-9", "Romans 3:23", "Romans 6:23", "Titus 3:5"] - }, - { - "title": "The Gospel Message", - "description": "The good news of Jesus Christ", - "slug": "gospel", - "verses": ["1 Corinthians 15:3-4", "Romans 1:16", "Mark 16:15", "Acts 4:12"] - } - ], - "Character & Living": [ - { - "title": "Fruits of the Spirit", - "description": "Developing Christian character", - "slug": "fruits-spirit", - "verses": ["Galatians 5:22-23", "1 Corinthians 13:4-7", "Philippians 4:8", "Colossians 3:12-14"] - }, - { - "title": "Prayer & Faith", - "description": "Growing in prayer and trust", - "slug": "prayer-faith", - "verses": ["Matthew 6:9-13", "1 Thessalonians 5:17", "Hebrews 11:1", "James 1:6"] - }, - { - "title": "Christian Living", - "description": "Walking as followers of Christ", - "slug": "christian-living", - "verses": ["Romans 12:1-2", "1 Peter 2:9", "Matthew 5:14-16", "Philippians 2:14-16"] - } - ], - "Biblical Themes": [ - { - "title": "God's Love", - "description": "Understanding the depth of God's love", - "slug": "gods-love", - "verses": ["1 John 4:8", "John 3:16", "Romans 8:38-39", "1 John 3:1"] - }, - { - "title": "Hope & Comfort", - "description": "Finding hope in difficult times", - "slug": "hope-comfort", - "verses": ["Romans 15:13", "2 Corinthians 1:3-4", "Psalm 23:4", "Isaiah 41:10"] - }, - { - "title": "Wisdom & Guidance", - "description": "Seeking God's wisdom for life", - "slug": "wisdom-guidance", - "verses": ["Proverbs 3:5-6", "James 1:5", "Psalm 119:105", "Proverbs 27:17"] - } - ], - "Doctrinal Studies": [ - { - "title": "The Trinity", - "description": "Understanding God as Father, Son, and Holy Spirit", - "slug": "trinity", - "verses": ["Matthew 28:19", "2 Corinthians 13:14", "1 Peter 1:2", "John 14:16-17"] - }, - { - "title": "The Resurrection", - "description": "Christ's victory over death and our hope", - "slug": "resurrection", - "verses": ["1 Corinthians 15:20-22", "Romans 6:4-5", "John 11:25-26", "1 Thessalonians 4:16-17"] - }, - { - "title": "Heaven & Eternity", - "description": "Our eternal home with God", - "slug": "heaven-eternity", - "verses": ["Revelation 21:1-4", "John 14:2-3", "Philippians 3:20-21", "1 Corinthians 2:9"] - } - ], - "Family & Relationships": [ - { - "title": "Biblical Marriage", - "description": "God's design for marriage", - "slug": "biblical-marriage", - "verses": ["Ephesians 5:22-33", "Genesis 2:24", "1 Corinthians 7:3-5", "Hebrews 13:4"] - }, - { - "title": "Raising Children", - "description": "Biblical principles for parenting", - "slug": "raising-children", - "verses": ["Proverbs 22:6", "Ephesians 6:4", "Deuteronomy 6:6-7", "Colossians 3:21"] - }, - { - "title": "Money & Stewardship", - "description": "Biblical wisdom on finances", - "slug": "money-stewardship", - "verses": ["Malachi 3:10", "Luke 16:10-11", "1 Timothy 6:10", "Proverbs 3:9-10"] - } - ] - } - - # Process verse references to add URLs - for category in study_guides.values(): - for guide in category: - guide['verse_refs'] = [ - { - 'text': verse, - 'url': verse_reference_to_url(verse) or '#' - } - for verse in guide['verses'] - ] - - return templates.TemplateResponse( - request, "index.html", {"books": books, "daily_verse": daily_verse, "study_guides": study_guides} - ) - - -@app.get("/books", response_class=HTMLResponse) -async def books_page(request: Request): - """Browse all books of the Bible""" - books = bible.get_books() - - # Define book categories with types - book_types = { - # Old Testament - 'Genesis': 'law', 'Exodus': 'law', 'Leviticus': 'law', 'Numbers': 'law', 'Deuteronomy': 'law', - 'Joshua': 'historical', 'Judges': 'historical', 'Ruth': 'historical', - '1 Samuel': 'historical', '2 Samuel': 'historical', '1 Kings': 'historical', '2 Kings': 'historical', - '1 Chronicles': 'historical', '2 Chronicles': 'historical', 'Ezra': 'historical', - 'Nehemiah': 'historical', 'Esther': 'historical', - 'Job': 'wisdom', 'Psalms': 'wisdom', 'Proverbs': 'wisdom', 'Ecclesiastes': 'wisdom', 'Song of Solomon': 'wisdom', - 'Isaiah': 'major-prophets', 'Jeremiah': 'major-prophets', 'Lamentations': 'major-prophets', - 'Ezekiel': 'major-prophets', 'Daniel': 'major-prophets', - 'Hosea': 'minor-prophets', 'Joel': 'minor-prophets', 'Amos': 'minor-prophets', - 'Obadiah': 'minor-prophets', 'Jonah': 'minor-prophets', 'Micah': 'minor-prophets', - 'Nahum': 'minor-prophets', 'Habakkuk': 'minor-prophets', 'Zephaniah': 'minor-prophets', - 'Haggai': 'minor-prophets', 'Zechariah': 'minor-prophets', 'Malachi': 'minor-prophets', - # New Testament - 'Matthew': 'gospels', 'Mark': 'gospels', 'Luke': 'gospels', 'John': 'gospels', - 'Acts': 'acts', - 'Romans': 'pauline', '1 Corinthians': 'pauline', '2 Corinthians': 'pauline', - 'Galatians': 'pauline', 'Ephesians': 'pauline', 'Philippians': 'pauline', 'Colossians': 'pauline', - '1 Thessalonians': 'pauline', '2 Thessalonians': 'pauline', - '1 Timothy': 'pauline', '2 Timothy': 'pauline', 'Titus': 'pauline', 'Philemon': 'pauline', - 'Hebrews': 'general', 'James': 'general', '1 Peter': 'general', '2 Peter': 'general', - '1 John': 'general', '2 John': 'general', '3 John': 'general', 'Jude': 'general', - 'Revelation': 'apocalyptic' - } - - # Organize books by testament - old_testament_books = [ - 'Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', 'Joshua', 'Judges', 'Ruth', - '1 Samuel', '2 Samuel', '1 Kings', '2 Kings', '1 Chronicles', '2 Chronicles', 'Ezra', - 'Nehemiah', 'Esther', 'Job', 'Psalms', 'Proverbs', 'Ecclesiastes', 'Song of Solomon', - 'Isaiah', 'Jeremiah', 'Lamentations', 'Ezekiel', 'Daniel', 'Hosea', 'Joel', 'Amos', - 'Obadiah', 'Jonah', 'Micah', 'Nahum', 'Habakkuk', 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi' - ] - - new_testament_books = [ - 'Matthew', 'Mark', 'Luke', 'John', 'Acts', 'Romans', '1 Corinthians', '2 Corinthians', - 'Galatians', 'Ephesians', 'Philippians', 'Colossians', '1 Thessalonians', '2 Thessalonians', - '1 Timothy', '2 Timothy', 'Titus', 'Philemon', 'Hebrews', 'James', '1 Peter', '2 Peter', - '1 John', '2 John', '3 John', 'Jude', 'Revelation' - ] - - # Get chapter counts for each book - def get_chapter_count(book_name): - chapters = bible.get_chapters_for_book(book_name) - return len(chapters) - - old_testament = [ - { - 'name': book, - 'chapters': get_chapter_count(book), - 'available': book in books, - 'type': book_types.get(book, '') - } - for book in old_testament_books - ] - - new_testament = [ - { - 'name': book, - 'chapters': get_chapter_count(book), - 'available': book in books, - 'type': book_types.get(book, '') - } - for book in new_testament_books - ] - - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Books", "url": None} - ] - - return templates.TemplateResponse( - request, - "books.html", - { - "old_testament": old_testament, - "new_testament": new_testament, - "books": books, - "breadcrumbs": breadcrumbs - } - ) - - -@app.get("/reading-plans", response_class=HTMLResponse) -async def reading_plans_page(request: Request): - """Browse Bible reading plans""" - books = bible.get_books() - plans = get_plan_summary() - - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Reading Plans", "url": None} - ] - - return templates.TemplateResponse( - request, - "reading_plans.html", - { - "plans": plans, - "books": books, - "breadcrumbs": breadcrumbs - } - ) - - -@app.get("/reading-plans/{plan_id}", response_class=HTMLResponse) -async def reading_plan_detail(request: Request, plan_id: str): - """View a specific reading plan""" - books = bible.get_books() - plan = get_plan(plan_id) - - if not plan: - raise HTTPException(status_code=404, detail="Reading plan not found") - - # Pass day info without text - text will be lazy loaded via API - all_days = plan.get('days') or plan.get('sample_days', []) - days_data = [] - for day in all_days: - day_data = { - 'day': day['day'], - 'theme': day.get('theme', ''), - 'readings': day['readings'] - } - days_data.append(day_data) - - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Reading Plans", "url": "/reading-plans"}, - {"text": plan["name"], "url": None} - ] - - return templates.TemplateResponse( - request, - "reading_plan_detail.html", - { - "plan": plan, - "plan_id": plan_id, - "books": books, - "breadcrumbs": breadcrumbs, - "pdf_available": WEASYPRINT_AVAILABLE, - "pdf_url": f"/reading-plans/{plan_id}/pdf" if WEASYPRINT_AVAILABLE else None, - "days_data": days_data, - "total_days": plan.get('duration_days', len(days_data)) - } - ) - - -def parse_reading_reference(ref: str) -> list: - """Parse a reading reference like 'Genesis 1-3' or 'Matthew 1' into chapter list. - - Returns list of tuples: [(book, chapter), ...] - """ - # Handle patterns like "Genesis 1-3", "Matthew 1", "1 John 2-3" - # Pattern: optional number prefix + book name + chapter range - pattern = r'^((?:\d\s+)?[A-Za-z]+(?:\s+[A-Za-z]+)?)\s+(\d+)(?:-(\d+))?$' - match = re.match(pattern, ref.strip()) - if not match: - return [] - - book = match.group(1) - start_ch = int(match.group(2)) - end_ch = int(match.group(3)) if match.group(3) else start_ch - - # Normalize book name - if it's already canonical, use it as-is - normalized = normalize_book_name(book) - if not normalized: - # Check if it's already a valid canonical name - all_books = OT_BOOKS + NT_BOOKS - if book in all_books: - normalized = book - else: - return [] - - return [(normalized, ch) for ch in range(start_ch, end_ch + 1)] - - -def get_reading_text(readings: list) -> list: - """Get the Bible text for a list of reading references. - - Returns list of dicts with book, chapter, and verses. - """ - result = [] - for ref in readings: - chapters = parse_reading_reference(ref) - for book, chapter in chapters: - verses = bible.get_verses_by_book_chapter(book, chapter) - if verses: - result.append({ - 'book': book, - 'chapter': chapter, - 'verses': verses, - 'reference': f"{book} {chapter}" - }) - return result - - -@app.get("/reading-plans/{plan_id}/pdf") -async def reading_plan_pdf(plan_id: str): - """Generate a PDF export for a reading plan.""" - if not WEASYPRINT_AVAILABLE: - raise HTTPException( - status_code=503, - detail="PDF generation is not available. WeasyPrint system libraries are not installed." - ) - - plan = get_plan(plan_id) - if not plan: - raise HTTPException(status_code=404, detail="Reading plan not found") - - # Include full Bible text for all plans (including 365-day plans) - include_text = True - - days_with_text = None - if include_text: - all_days = plan.get('days') or plan.get('sample_days', []) - days_with_text = [] - for day in all_days: - day_data = { - 'day': day['day'], - 'theme': day.get('theme', ''), - 'readings': day['readings'], - 'text': get_reading_text(day['readings']) - } - days_with_text.append(day_data) - - html_content = templates.get_template("reading_plan_pdf.html").render( - plan=plan, - include_text=include_text, - days_with_text=days_with_text - ) - pdf_buffer = await render_html_to_pdf_async(html_content) - - filename = f"reading-plan-{plan_id}.pdf" - return StreamingResponse( - pdf_buffer, - media_type="application/pdf", - headers={"Content-Disposition": f"attachment; filename={filename}"} - ) - - -@app.get("/topics", response_class=HTMLResponse) -async def topics_page(request: Request): - """Browse topical index of Bible themes""" - books = bible.get_books() - topics = get_all_topics() - - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Topics", "url": None} - ] - - return templates.TemplateResponse( - request, - "topics.html", - { - "topics": topics, - "books": books, - "breadcrumbs": breadcrumbs, - "pdf_available": WEASYPRINT_AVAILABLE - } - ) - - -@app.get("/resources", response_class=HTMLResponse) -async def resources_page(request: Request): - """Browse all theological resources""" - books = bible.get_books() - - # Organize resources into categories - resources = { - "People": [ - { - "name": "Biblical Prophets", - "url": "/biblical-prophets", - "description": "Explore the prophetic ministry throughout Scripture, from Isaiah to Malachi", - "count": "9 prophets" - }, - { - "name": "The Twelve Apostles", - "url": "/the-twelve-apostles", - "description": "The twelve disciples chosen by Jesus to be witnesses of His ministry", - "count": "12 apostles" - }, - { - "name": "Women of the Bible", - "url": "/women-of-the-bible", - "description": "Notable women of Scripture and their significance in redemptive history", - "count": "12 women" - } - ], - "Theology": [ - { - "name": "Biblical Angels", - "url": "/biblical-angels", - "description": "Angelic beings mentioned in Scripture, including Michael, Gabriel, and the heavenly host", - "count": "12 entries" - }, - { - "name": "The Tetragrammaton", - "url": "/tetragrammaton", - "description": "The sacred four-letter name of God (YHWH) and its profound significance", - "count": "Deep dive" - }, - { - "name": "Names of God", - "url": "/names-of-god", - "description": "The revelation of God's names throughout Scripture and their meanings", - "count": "14 names" - }, - { - "name": "Parables of Jesus", - "url": "/parables", - "description": "The parables spoken by Christ to illustrate spiritual truths", - "count": "11 parables" - }, - { - "name": "Miracles of Jesus", - "url": "/miracles-of-jesus", - "description": "Signs and wonders manifesting divine authority over nature, disease, demons, and death", - "count": "35+ miracles" - }, - { - "name": "I Am Statements", - "url": "/i-am-statements", - "description": "The seven 'I Am' statements of Jesus in John's Gospel revealing His divine nature", - "count": "7 statements" - }, - { - "name": "The Beatitudes", - "url": "/beatitudes", - "description": "The blessings proclaimed by Jesus in the Sermon on the Mount", - "count": "8 beatitudes" - }, - { - "name": "Ten Commandments", - "url": "/ten-commandments", - "description": "The moral law given by God to Moses on Mount Sinai", - "count": "10 commandments" - }, - { - "name": "Armor of God", - "url": "/armor-of-god", - "description": "The spiritual equipment for warfare described in Ephesians 6", - "count": "7 pieces" - }, - { - "name": "Prayers of the Bible", - "url": "/prayers-of-the-bible", - "description": "Sacred prayers from the Psalms, Jesus, Paul, and the early church", - "count": "20+ prayers" - }, - { - "name": "Biblical Covenants", - "url": "/biblical-covenants", - "description": "Divine covenants established between God and His people", - "count": "7 covenants" - }, - { - "name": "Fruits of the Spirit", - "url": "/fruits-of-the-spirit", - "description": "The nine graces of Galatians 5:22-23 manifested in believers through the Holy Spirit", - "count": "9 fruits" - } - ], - "Systematic Theology": [ - { - "name": "The Trinity", - "url": "/trinity", - "description": "The mystery of God revealed as Father, Son, and Holy Spirit—three Persons, one God", - "count": "4 categories" - }, - { - "name": "Christology", - "url": "/christology", - "description": "The Person and work of Jesus Christ—His deity, humanity, and offices", - "count": "4 categories" - }, - { - "name": "Pneumatology", - "url": "/pneumatology", - "description": "The doctrine of the Holy Spirit—His Person, deity, and work in believers", - "count": "4 categories" - }, - { - "name": "Soteriology", - "url": "/soteriology", - "description": "The doctrine of salvation—from election to glorification", - "count": "5 categories" - }, - { - "name": "Ecclesiology", - "url": "/ecclesiology", - "description": "The doctrine of the Church—its nature, mission, and governance", - "count": "4 categories" - }, - { - "name": "Eschatology", - "url": "/eschatology", - "description": "The doctrine of last things—Christ's return, judgment, and eternal state", - "count": "5 categories" - }, - { - "name": "The Kingdom of God", - "url": "/kingdom-of-god", - "description": "God's sovereign reign inaugurated in Christ and consummated at His return", - "count": "5 categories" - }, - { - "name": "Types and Shadows", - "url": "/types-and-shadows", - "description": "Old Testament persons, events, and institutions that prefigure Christ", - "count": "5 categories" - }, - { - "name": "Messianic Prophecies", - "url": "/messianic-prophecies", - "description": "Old Testament prophecies fulfilled in Jesus Christ", - "count": "5 categories" - }, - { - "name": "The Blood in Scripture", - "url": "/blood-in-scripture", - "description": "The theology of blood, sacrifice, and redemption throughout Scripture", - "count": "5 categories" - }, - { - "name": "Names and Titles of Christ", - "url": "/names-of-christ", - "description": "The names and titles ascribed to Jesus revealing His Person and work", - "count": "5 categories" - }, - { - "name": "Spirits & Demons", - "url": "/spirits-and-demons", - "description": "Biblical demonology—Satan, evil spirits, Legion, and spiritual warfare", - "count": "7 categories" - }, - { - "name": "Personifications", - "url": "/personifications", - "description": "Abstract concepts given human form—Wisdom, Folly, Death, Sin, and more", - "count": "6 categories" - }, - { - "name": "Bibliology", - "url": "/bibliology", - "description": "The Doctrine of Scripture—inspiration, authority, sufficiency, and preservation", - "count": "4 categories" - }, - { - "name": "Theology Proper", - "url": "/theology-proper", - "description": "The Attributes of God—His incommunicable and communicable perfections", - "count": "4 categories" - }, - { - "name": "Anthropology", - "url": "/anthropology", - "description": "The Doctrine of Man—creation, constitution, and condition of humanity", - "count": "4 categories" - }, - { - "name": "Hamartiology", - "url": "/hamartiology", - "description": "The Doctrine of Sin—its origin, nature, transmission, and consequences", - "count": "4 categories" - }, - { - "name": "Providence", - "url": "/providence", - "description": "Divine Providence—God's preservation, governance, and concurrence in all things", - "count": "4 categories" - }, - { - "name": "Grace", - "url": "/grace", - "description": "The Doctrine of Grace—common grace, effectual grace, election, and perseverance", - "count": "4 categories" - }, - { - "name": "Justification", - "url": "/justification", - "description": "The Doctrine of Justification—declared righteous through faith in Christ alone", - "count": "4 categories" - }, - { - "name": "Sanctification", - "url": "/sanctification", - "description": "The Doctrine of Sanctification—progressive holiness through the Spirit", - "count": "4 categories" - }, - { - "name": "Law and Gospel", - "url": "/law-and-gospel", - "description": "The distinction between Law and Gospel—God's demands and His gracious provision", - "count": "4 categories" - }, - { - "name": "Worship", - "url": "/worship", - "description": "The Doctrine of Worship—regulative principle, elements, and the heart of worship", - "count": "4 categories" - } - ], - "History & Culture": [ - { - "name": "Biblical Festivals", - "url": "/biblical-festivals", - "description": "The appointed feasts and holy days ordained in the Law of Moses", - "count": "7 festivals" - }, - { - "name": "Biblical Geography", - "url": "/biblical-maps", - "description": "Locations mentioned in Scripture and their historical significance", - "count": "Maps & places", - "badge": "Interactive" - }, - { - "name": "Biblical Timeline", - "url": "/biblical-timeline", - "description": "Chronological overview of biblical events from Creation to Revelation", - "count": "Timeline" - }, - { - "name": "Genealogies", - "url": "/family-tree", - "description": "Interactive family tree from Adam to Jesus Christ with detailed person profiles", - "count": "160+ people", - "badge": "Interactive" - } - ], - "Study Tools": [ - { - "name": "Study Guides", - "url": "/study-guides", - "description": "In-depth guides for studying biblical books, themes, and doctrines", - "count": "Multiple guides" - } - ] - } - - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Resources", "url": None} - ] - - return templates.TemplateResponse( - request, - "resources.html", - { - "resources": resources, - "books": books, - "breadcrumbs": breadcrumbs - } - ) - - -@app.get("/topics/{topic_name}", response_class=HTMLResponse) -async def topic_detail(request: Request, topic_name: str): - """View verses for a specific topic""" - books = bible.get_books() - topic = get_topic_with_text(topic_name) - - if not topic: - raise HTTPException(status_code=404, detail="Topic not found") - - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Topics", "url": "/topics"}, - {"text": topic_name, "url": None} - ] - - return templates.TemplateResponse( - request, - "topic_detail.html", - { - "topic": topic, - "topic_name": topic_name, - "books": books, - "breadcrumbs": breadcrumbs, - "pdf_available": WEASYPRINT_AVAILABLE, - "pdf_url": f"/topics/{topic_name}/pdf" if WEASYPRINT_AVAILABLE else None - } - ) - - -@app.get("/topics/{topic_name}/pdf") -async def topic_detail_pdf(topic_name: str): - """Generate a PDF export for a topic detail page.""" - if not WEASYPRINT_AVAILABLE: - raise HTTPException( - status_code=503, - detail="PDF generation is not available. WeasyPrint system libraries are not installed." - ) - - topic = get_topic_with_text(topic_name) - if not topic: - raise HTTPException(status_code=404, detail="Topic not found") - - html_content = templates.get_template("topic_pdf.html").render( - topic=topic, - topic_name=topic_name, - ) - pdf_buffer = await render_html_to_pdf_async(html_content) - - filename = f"{topic_name}.pdf" - return StreamingResponse( - pdf_buffer, - media_type="application/pdf", - headers={"Content-Disposition": f"attachment; filename={filename}"} - ) - - -@app.get("/book/{book}", response_class=HTMLResponse) -async def read_book(request: Request, book: str): - # Redirect book name variations to canonical form - canonical_name = normalize_book_name(book) - if canonical_name: - return RedirectResponse(url=f"/book/{canonical_name}", status_code=301) - - books = bible.get_books() - chapters = bible.get_chapters_for_book(book) - - if not chapters: - raise HTTPException( - status_code=404, - detail=f"The book '{book}' was not found. Please check the spelling or browse all available books." - ) - - # Generate commentary data for the book page - commentary_data = generate_book_commentary(book, chapters) - - # Calculate popularity scores for each chapter - chapter_popularity = {} - chapter_explanations = {} - for chapter in chapters: - chapter_popularity[chapter] = get_chapter_popularity_score(book, chapter) - chapter_explanations[chapter] = get_chapter_popularity_explanation(book, chapter) - - # Get book introduction data if available - book_intro = get_book_data(book) if has_book_data(book) else None - - # Build breadcrumbs - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Books", "url": "/books"}, - {"text": book, "url": None} - ] - - return templates.TemplateResponse( - request, - "book.html", - { - "book": book, - "chapters": chapters, - "books": books, - "chapter_popularity": chapter_popularity, - "chapter_explanations": chapter_explanations, - "breadcrumbs": breadcrumbs, - "current_book": book, - "pdf_available": WEASYPRINT_AVAILABLE, - "book_intro": book_intro, - **commentary_data - }, - ) - - -@app.get("/book/{book}/pdf") -async def book_pdf(request: Request, book: str): - """Generate a PDF export for an entire Bible book.""" - if not WEASYPRINT_AVAILABLE: - raise HTTPException( - status_code=503, - detail="PDF generation is not available. WeasyPrint system libraries are not installed." - ) - - canonical_name = normalize_book_name(book) - if canonical_name: - return RedirectResponse(url=f"/book/{canonical_name}/pdf", status_code=301) - - chapters = bible.get_chapters_for_book(book) - if not chapters: - raise HTTPException( - status_code=404, - detail=f"The book '{book}' was not found. Please check the spelling or browse all available books." - ) - - chapters_data = [] - total_verses = 0 - for chapter_num in chapters: - verses = bible.get_verses_by_book_chapter(book, chapter_num) - if not verses: - continue - total_verses += len(verses) - chapters_data.append({ - "chapter": chapter_num, - "verses": verses - }) - - if not chapters_data: - raise HTTPException( - status_code=404, - detail=f"No verses found for the book '{book}'." - ) - - # Get book introduction data if available - book_intro = get_book_data(book) if has_book_data(book) else None - - html_content = templates.get_template("book_pdf.html").render( - book=book, - chapters=chapters_data, - chapter_count=len(chapters_data), - verse_count=total_verses, - book_intro=book_intro, - ) - - pdf_buffer = await render_html_to_pdf_async(html_content) - filename = f"{create_slug(book)}.pdf" - - return StreamingResponse( - pdf_buffer, - media_type="application/pdf", - headers={"Content-Disposition": f"attachment; filename={filename}"} - ) - - -@app.get("/book/{book}/commentary") -def book_commentary_redirect(book: str): - """Redirect old book commentary URLs to book page""" - return RedirectResponse(url=f"/book/{book}", status_code=301) - - -@app.get("/book/{book}/{chapter}") -def redirect_chapter_legacy(book: str, chapter: int): - """Redirect legacy chapter URLs to correct format""" - return RedirectResponse(url=f"/book/{book}/chapter/{chapter}", status_code=301) - -@app.get("/book/{book}/chapter/{chapter}", response_class=HTMLResponse) -async def read_chapter(request: Request, book: str, chapter: int): - # Redirect book name variations to canonical form - canonical_name = normalize_book_name(book) - if canonical_name: - return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}", status_code=301) - - books = bible.get_books() - verses = bible.get_verses_by_book_chapter(book, chapter) - chapters = bible.get_chapters_for_book(book) - - if not verses: - # Check if the book exists first - if not chapters: - raise HTTPException( - status_code=404, - detail=f"The book '{book}' was not found. Please check the spelling or browse all available books." - ) - else: - raise HTTPException( - status_code=404, - detail=f"Chapter {chapter} of {book} was not found. This book has {len(chapters)} chapters." - ) - - - - # Generate AI commentary for the chapter - commentaries = {} - shown_words = set() # Track which words have already been shown in this chapter - for verse in verses: - commentary = generate_commentary(book, chapter, verse) - # Add word study sidenotes (avoiding repetition within chapter) - word_studies = generate_word_study_sidenotes(verse.text, book, chapter, verse.verse, shown_words) - commentary['word_studies'] = word_studies - # Track which words were shown - for study in word_studies: - shown_words.add(study['word'].lower()) - # Add cross-references with proper URLs, grouped by description - cross_refs = get_cross_references(book, chapter, verse.verse) - - # Group cross-references by their description/note - from collections import defaultdict - grouped_refs = defaultdict(list) - for ref in cross_refs: - description = ref['note'] if ref['note'] else 'Related' - # Parse the reference to build URL - if ' ' in ref['ref'] and ':' in ref['ref']: - ref_book = ref['ref'].rsplit(' ', 1)[0] - ref_chapter_verse = ref['ref'].rsplit(' ', 1)[1] - ref_chapter = ref_chapter_verse.split(':')[0] - ref_verse = ref_chapter_verse.split(':')[1] - # Same chapter: use anchor link; different chapter/book: link to chapter view with anchor - if ref_book == book and ref_chapter == str(chapter): - url = f"#verse-{ref_verse}" - else: - url = f"/book/{ref_book}/chapter/{ref_chapter}#verse-{ref_verse}" - else: - url = '#' - grouped_refs[description].append({ - 'text': ref['ref'], - 'url': url - }) - - # Convert to list of groups for template - commentary['cross_reference_groups'] = [ - {'description': desc, 'refs': refs} - for desc, refs in grouped_refs.items() - ] - commentaries[verse.verse] = commentary - - # Generate chapter overview - chapter_overview = generate_chapter_overview(book, chapter, verses) - - # Build breadcrumbs - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Books", "url": "/books"}, - {"text": book, "url": f"/book/{book}"}, - {"text": f"Chapter {chapter}", "url": None} - ] - - return templates.TemplateResponse( - request, - "chapter.html", - { - "book": book, - "chapter": chapter, - "verses": verses, - "books": books, - "chapters": chapters, - "commentaries": commentaries, - "chapter_overview": chapter_overview, - "breadcrumbs": breadcrumbs, - "current_book": book, - "current_chapter": chapter, - "pdf_available": WEASYPRINT_AVAILABLE - } - ) - - -@app.get("/book/{book}/chapter/{chapter}/pdf") -async def chapter_pdf(request: Request, book: str, chapter: int): - """Generate a PDF export for a specific Bible chapter.""" - if not WEASYPRINT_AVAILABLE: - raise HTTPException( - status_code=503, - detail="PDF generation is not available. WeasyPrint system libraries are not installed." - ) - - canonical_name = normalize_book_name(book) - if canonical_name: - return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}/pdf", status_code=301) - - verses = bible.get_verses_by_book_chapter(book, chapter) - chapters = bible.get_chapters_for_book(book) - - if not verses: - if not chapters: - raise HTTPException( - status_code=404, - detail=f"The book '{book}' was not found. Please check the spelling or browse all available books." - ) - raise HTTPException( - status_code=404, - detail=f"Chapter {chapter} of {book} was not found. This book has {len(chapters)} chapters." - ) - - # Generate commentaries with cross-references and word studies for PDF - commentaries = {} - shown_words = set() - for verse in verses: - commentary = generate_commentary(book, chapter, verse) - # Add word study sidenotes (avoiding repetition within chapter, more liberal for PDF) - word_studies = generate_word_study_sidenotes(verse.text, book, chapter, verse.verse, shown_words, for_pdf=True) - commentary['word_studies'] = word_studies - for study in word_studies: - shown_words.add(study['word'].lower()) - - # Add cross-references grouped by description - from collections import defaultdict - cross_refs = get_cross_references(book, chapter, verse.verse) - grouped_refs = defaultdict(list) - for ref in cross_refs: - description = ref['note'] if ref['note'] else 'Related' - grouped_refs[description].append(ref['ref']) - - commentary['cross_reference_groups'] = [ - {'description': desc, 'refs': refs} - for desc, refs in grouped_refs.items() - ] - commentaries[verse.verse] = commentary - - # Get book metadata for richer PDF - book_data = get_book_data(book) - total_chapters = len(chapters) - - # Collect all unique word studies shown for glossary - glossary = [] - seen_words = set() - for verse_num, commentary in commentaries.items(): - for study in commentary.get('word_studies', []): - word_lower = study['word'].lower() - if word_lower not in seen_words: - seen_words.add(word_lower) - glossary.append(study) - glossary.sort(key=lambda x: x['word']) - - html_content = templates.get_template("chapter_pdf.html").render( - book=book, - chapter=chapter, - verses=verses, - verse_count=len(verses), - commentaries=commentaries, - book_data=book_data, - total_chapters=total_chapters, - glossary=glossary, - ) - - pdf_buffer = await render_html_to_pdf_async(html_content) - filename = f"{create_slug(book)}-chapter-{chapter}.pdf" - - return StreamingResponse( - pdf_buffer, - media_type="application/pdf", - headers={"Content-Disposition": f"attachment; filename={filename}"} - ) - - -@app.get("/book/{book}/chapter/{chapter}/interlinear/pdf") -async def chapter_interlinear_pdf(book: str, chapter: int): - """Generate PDF export for interlinear chapter view.""" - # Redirect book name variations to canonical form - canonical_name = normalize_book_name(book) - if canonical_name: - return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}/interlinear/pdf", status_code=301) - - if not WEASYPRINT_AVAILABLE: - raise HTTPException( - status_code=503, - detail="PDF generation is not available. WeasyPrint system libraries are not installed." - ) - - verses = bible.get_verses_by_book_chapter(book, chapter) - chapters = bible.get_chapters_for_book(book) - - if not verses: - if not chapters: - raise HTTPException(status_code=404, detail=f"The book '{book}' was not found.") - else: - raise HTTPException(status_code=404, detail=f"Chapter {chapter} of {book} was not found.") - - # Get interlinear data for each verse - verses_with_interlinear = [] - for verse in verses: - interlinear_words = get_interlinear_data(book, chapter, verse.verse) - verses_with_interlinear.append({ - 'verse': verse, - 'interlinear_words': interlinear_words or [] - }) - - # Determine if OT or NT for language badge - is_old_testament = book in OT_BOOKS - - html_content = templates.get_template("chapter_interlinear_pdf.html").render( - book=book, - chapter=chapter, - verses_with_interlinear=verses_with_interlinear, - is_old_testament=is_old_testament - ) - pdf_buffer = await render_html_to_pdf_async(html_content) - - filename = f"{book.lower().replace(' ', '-')}-{chapter}-interlinear.pdf" - return StreamingResponse( - pdf_buffer, - media_type="application/pdf", - headers={"Content-Disposition": f"attachment; filename={filename}"} - ) - - -@app.get("/book/{book}/chapter/{chapter}/interlinear", response_class=HTMLResponse) -async def read_chapter_interlinear(request: Request, book: str, chapter: int): - """Display a chapter with interlinear Hebrew/Greek for every verse""" - # Redirect book name variations to canonical form - canonical_name = normalize_book_name(book) - if canonical_name: - return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}/interlinear", status_code=301) - - books = bible.get_books() - verses = bible.get_verses_by_book_chapter(book, chapter) - chapters = bible.get_chapters_for_book(book) - - if not verses: - if not chapters: - raise HTTPException( - status_code=404, - detail=f"The book '{book}' was not found." - ) - else: - raise HTTPException( - status_code=404, - detail=f"Chapter {chapter} of {book} was not found. This book has {len(chapters)} chapters." - ) - - # Get interlinear data for each verse - verses_with_interlinear = [] - for verse in verses: - interlinear_words = get_interlinear_data(book, chapter, verse.verse) - verses_with_interlinear.append({ - 'verse': verse, - 'interlinear_words': interlinear_words or [] - }) - - # Build breadcrumbs - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Books", "url": "/books"}, - {"text": book, "url": f"/book/{book}"}, - {"text": f"Chapter {chapter}", "url": f"/book/{book}/chapter/{chapter}"}, - {"text": "Interlinear", "url": None} - ] - - # Determine if OT or NT for language badge - ot_books = ['Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', 'Joshua', 'Judges', 'Ruth', - '1 Samuel', '2 Samuel', '1 Kings', '2 Kings', '1 Chronicles', '2 Chronicles', - 'Ezra', 'Nehemiah', 'Esther', 'Job', 'Psalms', 'Proverbs', 'Ecclesiastes', - 'Song of Solomon', 'Isaiah', 'Jeremiah', 'Lamentations', 'Ezekiel', 'Daniel', - 'Hosea', 'Joel', 'Amos', 'Obadiah', 'Jonah', 'Micah', 'Nahum', 'Habakkuk', - 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi'] - is_old_testament = book in ot_books - - return templates.TemplateResponse( - request, - "chapter_interlinear.html", - { - "book": book, - "chapter": chapter, - "verses_with_interlinear": verses_with_interlinear, - "books": books, - "chapters": chapters, - "breadcrumbs": breadcrumbs, - "current_book": book, - "current_chapter": chapter, - "is_old_testament": is_old_testament, - "pdf_available": WEASYPRINT_AVAILABLE, - "pdf_url": f"/book/{book}/chapter/{chapter}/interlinear/pdf" if WEASYPRINT_AVAILABLE else None - } - ) - - -@app.get("/book/{book}/chapter/{chapter}/verse/{verse_num}/pdf") -async def verse_pdf(book: str, chapter: int, verse_num: int): - """Generate PDF export for a single verse with commentary.""" - # Redirect book name variations to canonical form - canonical_name = normalize_book_name(book) - if canonical_name: - return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}/verse/{verse_num}/pdf", status_code=301) - - if not WEASYPRINT_AVAILABLE: - raise HTTPException( - status_code=503, - detail="PDF generation is not available. WeasyPrint system libraries are not installed." - ) - - verses = bible.get_verses_by_book_chapter(book, chapter) - if not verses: - raise HTTPException(status_code=404, detail=f"Chapter {chapter} of {book} was not found.") - - # Find the specific verse - verse = None - for v in verses: - if v.verse == verse_num: - verse = v - break - - if not verse: - raise HTTPException(status_code=404, detail=f"Verse {verse_num} not found in {book} {chapter}.") - - # Generate commentary - try: - commentary = generate_commentary(book, chapter, verse) - except Exception: - commentary = None - - # Get cross-references - cross_refs = get_cross_references(book, chapter, verse_num) - - # Get interlinear data - interlinear_words = get_interlinear_data(book, chapter, verse_num) - - # Determine if OT - is_ot = book in OT_BOOKS - - html_content = templates.get_template("verse_pdf.html").render( - book=book, - chapter=chapter, - verse_num=verse_num, - verse_text=verse.text, - commentary=commentary, - cross_references=cross_refs, - interlinear_words=interlinear_words, - is_old_testament=is_ot - ) - pdf_buffer = await render_html_to_pdf_async(html_content) - - filename = f"{book.lower().replace(' ', '-')}-{chapter}-{verse_num}.pdf" - return StreamingResponse( - pdf_buffer, - media_type="application/pdf", - headers={"Content-Disposition": f"attachment; filename={filename}"} - ) - - -@app.get("/book/{book}/chapter/{chapter}/verse/{verse_num}", response_class=HTMLResponse) -async def read_verse(request: Request, book: str, chapter: int, verse_num: int): - """Display a single verse with detailed commentary""" - # Redirect book name variations to canonical form - canonical_name = normalize_book_name(book) - if canonical_name: - return RedirectResponse(url=f"/book/{canonical_name}/chapter/{chapter}/verse/{verse_num}", status_code=301) - - books = bible.get_books() - verses = bible.get_verses_by_book_chapter(book, chapter) - chapters = bible.get_chapters_for_book(book) - - if not verses: - # Check if the book exists first - if not chapters: - raise HTTPException( - status_code=404, - detail=f"The book '{book}' was not found. Please check the spelling or browse all available books." - ) - else: - raise HTTPException( - status_code=404, - detail=f"Chapter {chapter} of {book} was not found. This book has {len(chapters)} chapters." - ) - - # Find the specific verse - verse = None - for v in verses: - if v.verse == verse_num: - verse = v - break - - if not verse: - raise HTTPException( - status_code=404, - detail=f"Verse {verse_num} not found in {book} {chapter}. This chapter has {len(verses)} verses." - ) - - # Generate commentary for this verse - try: - commentary = generate_commentary(book, chapter, verse) - except Exception as e: - # Log the error but don't fail the request - print(f"Error generating commentary for {book} {chapter}:{verse_num}: {e}") - commentary = None - - # Get cross-references for this verse - cross_refs = get_cross_references(book, chapter, verse_num) - - # Check if interlinear data is available and load it - has_interlinear = has_interlinear_data(book, chapter, verse_num) - interlinear_words = get_interlinear_data(book, chapter, verse_num) if has_interlinear else None - - # Get related content for internal linking - related_content = get_related_content(book, chapter, verse_num) - - # Build breadcrumbs - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Books", "url": "/books"}, - {"text": book, "url": f"/book/{book}"}, - {"text": f"Chapter {chapter}", "url": f"/book/{book}/chapter/{chapter}"}, - {"text": f"Verse {verse_num}", "url": None} - ] - - # Determine if Old Testament for interlinear styling - is_ot = book in OT_BOOKS - - return templates.TemplateResponse( - request, - "verse.html", - { - "book": book, - "chapter": chapter, - "verse_num": verse_num, - "verse_text": verse.text, - "commentary": commentary, - "cross_references": cross_refs, - "total_verses": len(verses), - "books": books, - "chapters": chapters, - "breadcrumbs": breadcrumbs, - "current_book": book, - "current_chapter": chapter, - "current_verse": verse_num, - "has_interlinear": has_interlinear, - "interlinear_words": interlinear_words, - "related_content": related_content, - "is_old_testament": is_ot, - "pdf_available": WEASYPRINT_AVAILABLE, - "pdf_url": f"/book/{book}/chapter/{chapter}/verse/{verse_num}/pdf" if WEASYPRINT_AVAILABLE else None - } - ) - - -# ============================================================================= -# Strong's Concordance Routes -# ============================================================================= - -@app.get("/strongs", response_class=HTMLResponse) -async def strongs_index(request: Request, q: str = None): - """Strong's Concordance search and lookup page.""" - results = [] - if q: - results = search_strongs(q, language="both", limit=100) - - books = bible.get_books() - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Strong's Concordance", "url": None} - ] - - return templates.TemplateResponse( - request, - "strongs_index.html", - { - "query": q or "", - "results": results, - "books": books, - "breadcrumbs": breadcrumbs - } - ) - - -@app.get("/strongs/hebrew", response_class=HTMLResponse) -async def strongs_hebrew_index(request: Request, page: int = 1): - """Paginated index of all Hebrew Strong's entries.""" - data = get_all_strongs("hebrew", page=page, per_page=100) - - books = bible.get_books() - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Strong's Concordance", "url": "/strongs"}, - {"text": "Hebrew", "url": None} - ] - - return templates.TemplateResponse( - request, - "strongs_language_index.html", - { - "language": "Hebrew", - "language_code": "hebrew", - "entries": data["entries"], - "page": data["page"], - "total_pages": data["total_pages"], - "total": data["total"], - "books": books, - "breadcrumbs": breadcrumbs - } - ) - - -@app.get("/strongs/greek", response_class=HTMLResponse) -async def strongs_greek_index(request: Request, page: int = 1): - """Paginated index of all Greek Strong's entries.""" - data = get_all_strongs("greek", page=page, per_page=100) - - books = bible.get_books() - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Strong's Concordance", "url": "/strongs"}, - {"text": "Greek", "url": None} - ] - - return templates.TemplateResponse( - request, - "strongs_language_index.html", - { - "language": "Greek", - "language_code": "greek", - "entries": data["entries"], - "page": data["page"], - "total_pages": data["total_pages"], - "total": data["total"], - "books": books, - "breadcrumbs": breadcrumbs - } - ) - - -@app.get("/strongs/{strongs_number}", response_class=HTMLResponse) -async def strongs_entry(request: Request, strongs_number: str): - """View a single Strong's concordance entry.""" - import re - - entry = format_strongs_entry(strongs_number) - - if not entry: - raise HTTPException( - status_code=404, - detail=f"Strong's number '{strongs_number}' not found" - ) - - # Find all verses containing this Strong's number - verse_occurrences = find_verses_by_strongs(strongs_number, limit=10000) - total_occurrences = len(verse_occurrences) - - # Fetch full verse text for each occurrence and highlight the word - for occ in verse_occurrences: - verse_text = bible.get_verse_text(occ["book"], occ["chapter"], occ["verse"]) - if verse_text: - # Highlight the English word in the verse text - english_word = occ.get("english", "") - if english_word and english_word in verse_text: - occ["verse_text"] = verse_text.replace( - english_word, - f'{english_word}', - 1 # Only highlight first occurrence - ) - else: - occ["verse_text"] = verse_text - else: - occ["verse_text"] = "" - - # Extract and fetch related Strong's entries from derivation - related_entries = [] - if entry.get("derivation"): - # Find all Strong's references like H1234 or G5678 - strongs_refs = re.findall(r'([HG])(\d+)', entry["derivation"]) - seen = set() - for prefix, num in strongs_refs: - ref = f"{prefix}{num}" - if ref.upper() != strongs_number.upper() and ref not in seen: - seen.add(ref) - related = format_strongs_entry(ref) - if related: - related_entries.append(related) - - books = bible.get_books() - breadcrumbs = [ - {"text": "Home", "url": "/"}, - {"text": "Strong's Concordance", "url": "/strongs"}, - {"text": strongs_number.upper(), "url": None} - ] - - return templates.TemplateResponse( - request, - "strongs_entry.html", - { - "entry": entry, - "books": books, - "breadcrumbs": breadcrumbs, - "verse_occurrences": verse_occurrences, - "total_occurrences": total_occurrences, - "related_entries": related_entries - } - ) +# Initialize the search_family_tree function in misc routes +init_search_family_tree(search_family_tree)