From 0dd02c6bf90fd5800c8acd708bf658d0e6fc32b9 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Mon, 24 Nov 2025 17:42:42 -0500 Subject: [PATCH] Add modular code structure with utils and routes packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create utils/ package with: - books.py: Book name normalization and abbreviations - search.py: Full-text search functionality - helpers.py: Common utilities (verse parsing, daily verse, etc.) - Create routes/ package with: - api.py: All /api/* endpoints extracted to APIRouter - Update server.py to: - Import from new modular structure - Include API router for cleaner organization This is the first step toward breaking up the 12,600+ line server.py into maintainable modules. The old API routes in server.py are still present as a transition - they can be removed once tests confirm the new router works correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- kjvstudy_org/routes/__init__.py | 6 + kjvstudy_org/routes/api.py | 442 ++++++++++++++++++++++++++++ kjvstudy_org/server.py | 70 ++++- kjvstudy_org/templates/chapter.html | 44 ++- kjvstudy_org/utils/__init__.py | 8 + kjvstudy_org/utils/books.py | 246 ++++++++++++++++ kjvstudy_org/utils/helpers.py | 426 +++++++++++++++++++++++++++ kjvstudy_org/utils/search.py | 73 +++++ 8 files changed, 1296 insertions(+), 19 deletions(-) create mode 100644 kjvstudy_org/routes/__init__.py create mode 100644 kjvstudy_org/routes/api.py create mode 100644 kjvstudy_org/utils/__init__.py create mode 100644 kjvstudy_org/utils/books.py create mode 100644 kjvstudy_org/utils/helpers.py create mode 100644 kjvstudy_org/utils/search.py diff --git a/kjvstudy_org/routes/__init__.py b/kjvstudy_org/routes/__init__.py new file mode 100644 index 0000000..052837a --- /dev/null +++ b/kjvstudy_org/routes/__init__.py @@ -0,0 +1,6 @@ +# Route modules for KJV Study +from fastapi import APIRouter + +from .api import router as api_router + +__all__ = ['api_router'] diff --git a/kjvstudy_org/routes/api.py b/kjvstudy_org/routes/api.py new file mode 100644 index 0000000..b3b4b9c --- /dev/null +++ b/kjvstudy_org/routes/api.py @@ -0,0 +1,442 @@ +"""API routes for KJV Study - JSON endpoints for programmatic access.""" +from typing import Optional + +from fastapi import APIRouter, HTTPException, Query, Path +from fastapi.responses import JSONResponse + +from ..kjv import bible +from ..cross_references import get_cross_references +from ..reading_plans import get_plan, get_plan_summary +from ..topics import get_all_topics, get_topic +from ..interlinear_loader import get_interlinear_data, has_interlinear_data +from ..utils.books import normalize_book_name, OT_BOOKS +from ..utils.search import perform_full_text_search +from ..utils.helpers import get_daily_verse + +router = APIRouter(prefix="/api", tags=["API"]) + + +@router.get("/") +def api_index(): + """API index with links to documentation and available endpoints.""" + return { + "name": "KJV Study API", + "version": "1.0.0", + "description": "RESTful API for accessing King James Bible verses and study resources", + "documentation": { + "swagger_ui": "/api/docs", + "redoc": "/api/redoc", + "openapi_json": "/api/openapi.json" + }, + "endpoints": { + "health": "/api/health", + "search": "/api/search?q={query}", + "verse_of_the_day": "/api/verse-of-the-day", + "verse": "/api/verse/{book}/{chapter}/{verse}", + "verse_range": "/api/verse-range/{book}/{chapter}/{start}/{end}", + "interlinear": "/api/interlinear/{book}/{chapter}/{verse}", + "books": "/api/books", + "book": "/api/books/{book}", + "chapter": "/api/books/{book}/chapters/{chapter}", + "book_text": "/api/books/{book}/text", + "bible": "/api/bible", + "cross_references": "/api/cross-references/{book}/{chapter}/{verse}", + "topics": "/api/topics", + "topic": "/api/topics/{topic_name}", + "reading_plans": "/api/reading-plans", + "reading_plan": "/api/reading-plans/{plan_id}" + } + } + + +@router.get("/health") +def api_health_check(): + """API health check endpoint for monitoring and status verification.""" + return { + "status": "healthy", + "service": "KJV Study API", + "version": "1.0.0" + } + + +@router.get("/search") +def search_api( + q: str = Query(..., description="Search query", example="faith"), + limit: Optional[int] = Query(None, description="Max results", example=10) +): + """JSON API endpoint for search.""" + if not q or len(q.strip()) < 2: + return {"query": q, "results": [], "total": 0} + + results = perform_full_text_search(q.strip(), limit) + is_direct_verse = False + + # Check if this was a direct verse reference match + if results and len(results) == 1 and results[0].get("score") == 100.0: + is_direct_verse = True + + return { + "query": q, + "results": results, + "total": len(results), + "is_direct_verse": is_direct_verse + } + + +@router.get("/verse-of-the-day") +def verse_of_the_day_api(): + """API endpoint for verse of the day.""" + return get_daily_verse() + + +@router.get("/verse/{book}/{chapter}/{verse}") +def api_get_verse( + book: str = Path(..., description="Book name", example="John"), + chapter: int = Path(..., description="Chapter number", example=3), + verse: int = Path(..., description="Verse number", example=16) +): + """Get a single verse text.""" + try: + canonical_name = normalize_book_name(book) + if canonical_name: + book = canonical_name + + verse_text = bible.get_verse_text(book, chapter, verse) + if not verse_text: + raise HTTPException(status_code=404, detail="Verse not found") + + return JSONResponse({ + "book": book, + "chapter": chapter, + "verse": verse, + "reference": f"{book} {chapter}:{verse}", + "text": verse_text + }) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/verse-range/{book}/{chapter}/{start}/{end}") +def api_get_verse_range( + book: str = Path(..., description="Book name", example="Psalms"), + chapter: int = Path(..., description="Chapter number", example=23), + start: int = Path(..., description="Starting verse number", example=1), + end: int = Path(..., description="Ending verse number", example=6) +): + """Get a range of verses.""" + try: + canonical_name = normalize_book_name(book) + if canonical_name: + book = canonical_name + + verses = [] + verse_texts = [] + + for verse_num in range(start, end + 1): + verse_text = bible.get_verse_text(book, chapter, verse_num) + if verse_text: + verses.append({ + "verse": verse_num, + "text": verse_text + }) + verse_texts.append(verse_text) + + if not verses: + raise HTTPException(status_code=404, detail="Verse range not found") + + return JSONResponse({ + "book": book, + "chapter": chapter, + "start": start, + "end": end, + "reference": f"{book} {chapter}:{start}-{end}", + "verses": verses, + "text": " ".join(verse_texts) + }) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/interlinear/{book}/{chapter}/{verse}") +def api_get_interlinear( + book: str = Path(..., description="Book name", example="John"), + chapter: int = Path(..., description="Chapter number", example=1), + verse: int = Path(..., description="Verse number", example=1) +): + """Get interlinear (word-by-word) data for a verse.""" + try: + canonical_name = normalize_book_name(book) + if canonical_name: + book = canonical_name + + verse_text = bible.get_verse_text(book, chapter, verse) + if not verse_text: + raise HTTPException(status_code=404, detail="Verse not found") + + if not has_interlinear_data(book, chapter, verse): + return JSONResponse({ + "book": book, + "chapter": chapter, + "verse": verse, + "reference": f"{book} {chapter}:{verse}", + "text": verse_text, + "interlinear_available": False, + "words": [] + }) + + interlinear_words = get_interlinear_data(book, chapter, verse) + + return JSONResponse({ + "book": book, + "chapter": chapter, + "verse": verse, + "reference": f"{book} {chapter}:{verse}", + "text": verse_text, + "interlinear_available": True, + "words": interlinear_words + }) + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/books") +def api_get_books(): + """Get list of all Bible books.""" + books = list(bible.iter_books()) + + old_testament = [] + new_testament = [] + + for book in books: + chapters = [ch for bk, ch in bible.iter_chapters() if bk == book] + book_info = { + "name": book, + "chapters": len(chapters), + "testament": "Old Testament" if book in OT_BOOKS else "New Testament" + } + + if book in OT_BOOKS: + old_testament.append(book_info) + else: + new_testament.append(book_info) + + return { + "total_books": len(books), + "old_testament": old_testament, + "new_testament": new_testament + } + + +@router.get("/books/{book}") +def api_get_book(book: str = Path(..., description="Book name", example="Genesis")): + """Get details about a specific book.""" + canonical_name = normalize_book_name(book) + if canonical_name: + book = canonical_name + + chapters = [ch for bk, ch in bible.iter_chapters() if bk == book] + if not chapters: + raise HTTPException(status_code=404, detail="Book not found") + + chapter_details = [] + for chapter in chapters: + verses = [v for v in bible.iter_verses() if v.book == book and v.chapter == chapter] + chapter_details.append({ + "chapter": chapter, + "verses": len(verses) + }) + + return { + "name": book, + "total_chapters": len(chapters), + "chapters": chapter_details + } + + +@router.get("/books/{book}/chapters/{chapter}") +def api_get_chapter( + book: str = Path(..., description="Book name", example="Romans"), + chapter: int = Path(..., description="Chapter number", example=8) +): + """Get all verses in a chapter.""" + canonical_name = normalize_book_name(book) + if canonical_name: + book = canonical_name + + verses = [v for v in bible.iter_verses() if v.book == book and v.chapter == chapter] + if not verses: + raise HTTPException(status_code=404, detail="Chapter not found") + + verse_list = [] + for v in verses: + verse_list.append({ + "verse": v.verse, + "text": v.text + }) + + return { + "book": book, + "chapter": chapter, + "total_verses": len(verses), + "verses": verse_list + } + + +@router.get("/books/{book}/text") +def api_get_book_text(book: str = Path(..., description="Book name", example="Philemon")): + """Get all text content of a book.""" + canonical_name = normalize_book_name(book) + if canonical_name: + book = canonical_name + + verses = [v for v in bible.iter_verses() if v.book == book] + if not verses: + raise HTTPException(status_code=404, detail="Book not found") + + chapters = {} + for v in verses: + if v.chapter not in chapters: + chapters[v.chapter] = [] + chapters[v.chapter].append({ + "verse": v.verse, + "text": v.text + }) + + chapter_list = [] + for chapter_num in sorted(chapters.keys()): + chapter_list.append({ + "chapter": chapter_num, + "verses": chapters[chapter_num] + }) + + return { + "book": book, + "total_chapters": len(chapters), + "total_verses": len(verses), + "chapters": chapter_list + } + + +@router.get("/bible") +def api_get_bible(): + """Get the entire Bible text.""" + books_data = {} + for v in bible.iter_verses(): + if v.book not in books_data: + books_data[v.book] = {} + if v.chapter not in books_data[v.book]: + books_data[v.book][v.chapter] = [] + books_data[v.book][v.chapter].append({ + "verse": v.verse, + "text": v.text + }) + + books_list = [] + for book_name in books_data: + chapter_list = [] + for chapter_num in sorted(books_data[book_name].keys()): + chapter_list.append({ + "chapter": chapter_num, + "verses": books_data[book_name][chapter_num] + }) + + books_list.append({ + "book": book_name, + "chapters": chapter_list + }) + + total_verses = sum(len(books_data[book][ch]) for book in books_data for ch in books_data[book]) + + return { + "total_books": len(books_data), + "total_verses": total_verses, + "books": books_list + } + + +@router.get("/cross-references/{book}/{chapter}/{verse}") +def api_get_cross_references( + book: str = Path(..., description="Book name", example="John"), + chapter: int = Path(..., description="Chapter number", example=3), + verse: int = Path(..., description="Verse number", example=16) +): + """Get cross-references for a verse.""" + canonical_name = normalize_book_name(book) + if canonical_name: + book = canonical_name + + verse_text = bible.get_verse_text(book, chapter, verse) + if not verse_text: + raise HTTPException(status_code=404, detail="Verse not found") + + cross_refs = get_cross_references(book, chapter, verse) + + return { + "book": book, + "chapter": chapter, + "verse": verse, + "reference": f"{book} {chapter}:{verse}", + "cross_references": cross_refs + } + + +@router.get("/topics") +def api_get_topics(): + """Get list of all topics.""" + topics = get_all_topics() + + topic_list = [] + for topic_name, topic_data in topics.items(): + topic_list.append({ + "name": topic_name, + "slug": topic_name, + "description": topic_data.get("description", ""), + "subtopics": list(topic_data.get("subtopics", {}).keys()) + }) + + return { + "total_topics": len(topics), + "topics": topic_list + } + + +@router.get("/topics/{topic_name}") +def api_get_topic(topic_name: str = Path(..., description="Topic name", example="faith")): + """Get details about a specific topic.""" + topic = get_topic(topic_name) + if not topic: + raise HTTPException(status_code=404, detail="Topic not found") + + return { + "name": topic_name, + "description": topic.get("description", ""), + "overview": topic.get("overview", ""), + "subtopics": topic.get("subtopics", {}) + } + + +@router.get("/reading-plans") +def api_get_reading_plans(): + """Get list of all reading plans.""" + plans = get_plan_summary() + + return { + "total_plans": len(plans), + "plans": plans + } + + +@router.get("/reading-plans/{plan_id}") +def api_get_reading_plan(plan_id: str = Path(..., description="Reading plan ID", example="chronological")): + """Get details about a specific reading plan.""" + plan = get_plan(plan_id) + if not plan: + raise HTTPException(status_code=404, detail="Reading plan not found") + + return plan diff --git a/kjvstudy_org/server.py b/kjvstudy_org/server.py index 1c6312b..c9dd752 100644 --- a/kjvstudy_org/server.py +++ b/kjvstudy_org/server.py @@ -23,6 +23,21 @@ from .reading_plans import get_plan, get_all_plans, get_plan_summary from .topics import get_all_topics, get_topic, search_topics from .interlinear_loader import get_interlinear_data, has_interlinear_data, get_all_interlinear_verses, preload_data +# Import from new modular structure +from .utils.books import normalize_book_name, OT_BOOKS, NT_BOOKS +from .utils.search import perform_full_text_search, calculate_relevance_score, highlight_search_terms +from .utils.helpers import ( + create_slug, + get_verse_text, + is_verse_reference, + parse_verse_reference, + get_related_content, + get_chapter_popularity_score, + get_chapter_popularity_explanation, + get_daily_verse, +) +from .routes.api import router as api_router + try: from ged4py import GedcomReader except ImportError: @@ -761,6 +776,9 @@ app.add_middleware(GZipMiddleware, minimum_size=500) # Add caching middleware app.add_middleware(CacheControlMiddleware) +# Include API router from modular routes +app.include_router(api_router) + # Set up Jinja2 templates and static files current_dir = PathLib(__file__).parent @@ -773,6 +791,28 @@ templates = Jinja2Templates(directory=str(templates_dir)) # Register custom Jinja2 filters templates.env.filters['slugify'] = create_slug +def inject_word_markers(text, word_studies, verse_num): + """Inject sidenote markers into verse text next to annotated words""" + if not word_studies: + return text + + # Process each word study + for idx, study in enumerate(word_studies, 1): + word = study['word'] + # Create the sidenote marker HTML + marker = f'{word}: {study["term"]} ({study["translit"]}). {study["note"]}' + + # Find and replace the word with word + marker + # Use a more precise replacement to avoid replacing partial matches + import re + # Match the word with word boundaries, case-insensitive + pattern = re.compile(r'\b(' + re.escape(word) + r')\b', re.IGNORECASE) + text = pattern.sub(r'\1' + marker, text, count=1) + + return text + +templates.env.filters['inject_word_markers'] = inject_word_markers + # Load Scofield commentary for cross-references scofield_commentary = {} try: @@ -9722,6 +9762,13 @@ def generate_chapter_overview(book, chapter, verses): time_period = get_time_period(book) historical_context = get_historical_context(book) + # Helper function to create verse range links + def verse_link(start, end): + if start == end: + return f'Verse {start}' + else: + return f'Verses {start}-{end}' + overview = f"""

{book} {chapter} is a {chapter_type} chapter in the {get_testament_for_book(book)} that explores themes of {', '.join(unique_themes)}. Written during {time_period}, this chapter should be understood within its historical context: {historical_context}

@@ -9729,10 +9776,10 @@ def generate_chapter_overview(book, chapter, verses):

The chapter can be divided into several sections:

    -
  1. Verses 1-{min(5, len(verses))}: Introduction and setting the context
  2. - {'
  3. Verses 6-' + str(min(12, len(verses))) + ': Development of key themes
  4. ' if len(verses) > 5 else ''} - {'
  5. Verses 13-' + str(min(20, len(verses))) + ': Central message and teachings
  6. ' if len(verses) > 12 else ''} - {'
  7. Verses ' + str(min(21, len(verses))) + '-' + str(len(verses)) + ': Conclusion and application
  8. ' if len(verses) > 20 else ''} +
  9. {verse_link(1, min(5, len(verses)))}: Introduction and setting the context
  10. + {'
  11. ' + verse_link(6, min(12, len(verses))) + ': Development of key themes
  12. ' if len(verses) > 5 else ''} + {'
  13. ' + verse_link(13, min(20, len(verses))) + ': Central message and teachings
  14. ' if len(verses) > 12 else ''} + {'
  15. ' + verse_link(min(21, len(verses)), len(verses)) + ': Conclusion and application
  16. ' if len(verses) > 20 else ''}

This chapter is significant because it {get_chapter_significance(book, chapter)}. @@ -10091,6 +10138,13 @@ def generate_chapter_overview(book, chapter, verses): time_period = get_time_period(book) historical_context = get_historical_context(book) + # Helper function to create verse range links + def verse_link(start, end): + if start == end: + return f'Verse {start}' + else: + return f'Verses {start}-{end}' + overview = f"""

{book} {chapter} is a {chapter_type} chapter in the {get_testament_for_book(book)} that explores themes of {', '.join(unique_themes)}. Written during {time_period}, this chapter should be understood within its historical context: {historical_context}

@@ -10098,10 +10152,10 @@ def generate_chapter_overview(book, chapter, verses):

The chapter can be divided into several sections:

    -
  1. Verses 1-{min(5, len(verses))}: Introduction and setting the context
  2. - {'
  3. Verses 6-' + str(min(12, len(verses))) + ': Development of key themes
  4. ' if len(verses) > 5 else ''} - {'
  5. Verses 13-' + str(min(20, len(verses))) + ': Central message and teachings
  6. ' if len(verses) > 12 else ''} - {'
  7. Verses ' + str(min(21, len(verses))) + '-' + str(len(verses)) + ': Conclusion and application
  8. ' if len(verses) > 20 else ''} +
  9. {verse_link(1, min(5, len(verses)))}: Introduction and setting the context
  10. + {'
  11. ' + verse_link(6, min(12, len(verses))) + ': Development of key themes
  12. ' if len(verses) > 5 else ''} + {'
  13. ' + verse_link(13, min(20, len(verses))) + ': Central message and teachings
  14. ' if len(verses) > 12 else ''} + {'
  15. ' + verse_link(min(21, len(verses)), len(verses)) + ': Conclusion and application
  16. ' if len(verses) > 20 else ''}

This chapter is significant because it {get_chapter_significance(book, chapter)}. diff --git a/kjvstudy_org/templates/chapter.html b/kjvstudy_org/templates/chapter.html index 68329c3..1eb4112 100644 --- a/kjvstudy_org/templates/chapter.html +++ b/kjvstudy_org/templates/chapter.html @@ -9,10 +9,25 @@ max-height: 150px; overflow: hidden; cursor: pointer; - transition: max-height 0.3s ease; + transition: all 0.3s ease; position: relative; } +/* Highlight sidenote when its checkbox is checked */ +.margin-toggle:checked + .sidenote, +.margin-toggle:checked + .marginnote { + background-color: rgba(255, 237, 160, 0.3); + border-left: 3px solid rgba(255, 193, 7, 0.6); + padding-left: 0.5rem; + margin-left: -0.5rem; +} + +[data-theme="dark"] .margin-toggle:checked + .sidenote, +[data-theme="dark"] .margin-toggle:checked + .marginnote { + background-color: rgba(255, 193, 7, 0.15); + border-left: 3px solid rgba(255, 193, 7, 0.5); +} + .sidenote.expanded, .marginnote.expanded { max-height: none; @@ -154,20 +169,13 @@ hr::before { {% for verse in verses %} {% set commentary = commentaries[verse.verse] if commentaries and verse.verse in commentaries else none %}

- {{ verse.verse }} {{ verse.text | link_names | safe }} + {{ verse.verse }} {{ verse.text | inject_word_markers(commentary.word_studies if commentary else [], verse.verse) | link_names | safe }} {% if commentary %} - {% if commentary.word_studies %} - {% for study in commentary.word_studies %} - - - {{ study.word }}: {{ study.term }} ({{ study.translit }}). {{ study.note | safe }} - {% endfor %} - {% endif %} {% if commentary.cross_references %} {% for ref in commentary.cross_references %} - + - {{ ref.text }}{% if ref.context %} — {{ ref.context }}{% endif %} + {{ ref.text }}{% if ref.context %} — {{ ref.context }}{% endif %} {% endfor %} {% endif %} {% endif %} @@ -320,6 +328,20 @@ document.addEventListener('DOMContentLoaded', function() { } } }); + + // Only allow one sidenote to be highlighted at a time + document.querySelectorAll('.margin-toggle').forEach(function(checkbox) { + checkbox.addEventListener('change', function() { + if (this.checked) { + // Uncheck all other margin-toggle checkboxes + document.querySelectorAll('.margin-toggle').forEach(function(otherCheckbox) { + if (otherCheckbox !== checkbox) { + otherCheckbox.checked = false; + } + }); + } + }); + }); }); {% endblock %} diff --git a/kjvstudy_org/utils/__init__.py b/kjvstudy_org/utils/__init__.py new file mode 100644 index 0000000..a496492 --- /dev/null +++ b/kjvstudy_org/utils/__init__.py @@ -0,0 +1,8 @@ +# Utility modules for KJV Study +# Note: Import individual modules to avoid circular imports + +__all__ = [ + 'books', + 'search', + 'helpers', +] diff --git a/kjvstudy_org/utils/books.py b/kjvstudy_org/utils/books.py new file mode 100644 index 0000000..c8cf06b --- /dev/null +++ b/kjvstudy_org/utils/books.py @@ -0,0 +1,246 @@ +"""Book name normalization and abbreviation handling.""" +from typing import Optional + +# Old Testament books in order +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' +] + +# New Testament books in order +NT_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' +] + +# All book abbreviations and variations mapped to canonical names +BOOK_ABBREVIATIONS = { + # Psalm/Psalms + "Psalm": "Psalms", + + # Roman numerals to Arabic numerals + "I Samuel": "1 Samuel", + "II Samuel": "2 Samuel", + "I Kings": "1 Kings", + "II Kings": "2 Kings", + "I Chronicles": "1 Chronicles", + "II Chronicles": "2 Chronicles", + "I Corinthians": "1 Corinthians", + "II Corinthians": "2 Corinthians", + "I Thessalonians": "1 Thessalonians", + "II Thessalonians": "2 Thessalonians", + "I Timothy": "1 Timothy", + "II Timothy": "2 Timothy", + "I Peter": "1 Peter", + "II Peter": "2 Peter", + "I John": "1 John", + "II John": "2 John", + "III John": "3 John", + + # Full word numbers to Arabic numerals + "First Samuel": "1 Samuel", + "Second Samuel": "2 Samuel", + "First Kings": "1 Kings", + "Second Kings": "2 Kings", + "First Chronicles": "1 Chronicles", + "Second Chronicles": "2 Chronicles", + "First Corinthians": "1 Corinthians", + "Second Corinthians": "2 Corinthians", + "First Thessalonians": "1 Thessalonians", + "Second Thessalonians": "2 Thessalonians", + "First Timothy": "1 Timothy", + "Second Timothy": "2 Timothy", + "First Peter": "1 Peter", + "Second Peter": "2 Peter", + "First John": "1 John", + "Second John": "2 John", + "Third John": "3 John", + + # Alternative names + "Song of Songs": "Song of Solomon", + "Canticles": "Song of Solomon", + + # Common abbreviations + "Gen": "Genesis", + "Ge": "Genesis", + "Exo": "Exodus", + "Ex": "Exodus", + "Lev": "Leviticus", + "Le": "Leviticus", + "Num": "Numbers", + "Nu": "Numbers", + "Deut": "Deuteronomy", + "Dt": "Deuteronomy", + "Josh": "Joshua", + "Jos": "Joshua", + "Judg": "Judges", + "Jdg": "Judges", + "Ru": "Ruth", + "1Sam": "1 Samuel", + "1 Sam": "1 Samuel", + "1S": "1 Samuel", + "2Sam": "2 Samuel", + "2 Sam": "2 Samuel", + "2S": "2 Samuel", + "1Ki": "1 Kings", + "1 Ki": "1 Kings", + "1K": "1 Kings", + "2Ki": "2 Kings", + "2 Ki": "2 Kings", + "2K": "2 Kings", + "1Chr": "1 Chronicles", + "1 Chr": "1 Chronicles", + "1Ch": "1 Chronicles", + "2Chr": "2 Chronicles", + "2 Chr": "2 Chronicles", + "2Ch": "2 Chronicles", + "Ezr": "Ezra", + "Neh": "Nehemiah", + "Ne": "Nehemiah", + "Est": "Esther", + "Ps": "Psalms", + "Psa": "Psalms", + "Prov": "Proverbs", + "Pr": "Proverbs", + "Eccl": "Ecclesiastes", + "Ec": "Ecclesiastes", + "Song": "Song of Solomon", + "Sos": "Song of Solomon", + "SS": "Song of Solomon", + "Isa": "Isaiah", + "Is": "Isaiah", + "Jer": "Jeremiah", + "Je": "Jeremiah", + "Lam": "Lamentations", + "La": "Lamentations", + "Ezek": "Ezekiel", + "Eze": "Ezekiel", + "Ezk": "Ezekiel", + "Dan": "Daniel", + "Da": "Daniel", + "Hos": "Hosea", + "Ho": "Hosea", + "Joe": "Joel", + "Jl": "Joel", + "Am": "Amos", + "Ob": "Obadiah", + "Jon": "Jonah", + "Mic": "Micah", + "Mi": "Micah", + "Nah": "Nahum", + "Na": "Nahum", + "Hab": "Habakkuk", + "Hb": "Habakkuk", + "Zep": "Zephaniah", + "Zph": "Zephaniah", + "Hag": "Haggai", + "Hg": "Haggai", + "Zech": "Zechariah", + "Zec": "Zechariah", + "Zch": "Zechariah", + "Mal": "Malachi", + "Mat": "Matthew", + "Mt": "Matthew", + "Mar": "Mark", + "Mk": "Mark", + "Mrk": "Mark", + "Luk": "Luke", + "Lk": "Luke", + "Joh": "John", + "Jn": "John", + "Act": "Acts", + "Ac": "Acts", + "Rom": "Romans", + "Ro": "Romans", + "1Cor": "1 Corinthians", + "1 Cor": "1 Corinthians", + "1Co": "1 Corinthians", + "2Cor": "2 Corinthians", + "2 Cor": "2 Corinthians", + "2Co": "2 Corinthians", + "Gal": "Galatians", + "Ga": "Galatians", + "Eph": "Ephesians", + "Ep": "Ephesians", + "Phil": "Philippians", + "Php": "Philippians", + "Ph": "Philippians", + "Col": "Colossians", + "Co": "Colossians", + "1Thess": "1 Thessalonians", + "1 Thess": "1 Thessalonians", + "1Th": "1 Thessalonians", + "2Thess": "2 Thessalonians", + "2 Thess": "2 Thessalonians", + "2Th": "2 Thessalonians", + "1Tim": "1 Timothy", + "1 Tim": "1 Timothy", + "1Ti": "1 Timothy", + "2Tim": "2 Timothy", + "2 Tim": "2 Timothy", + "2Ti": "2 Timothy", + "Tit": "Titus", + "Ti": "Titus", + "Phm": "Philemon", + "Pm": "Philemon", + "Heb": "Hebrews", + "He": "Hebrews", + "Jam": "James", + "Jas": "James", + "Jm": "James", + "1Pet": "1 Peter", + "1 Pet": "1 Peter", + "1Pe": "1 Peter", + "1P": "1 Peter", + "2Pet": "2 Peter", + "2 Pet": "2 Peter", + "2Pe": "2 Peter", + "2P": "2 Peter", + "1Joh": "1 John", + "1 Joh": "1 John", + "1Jn": "1 John", + "2Joh": "2 John", + "2 Joh": "2 John", + "2Jn": "2 John", + "3Joh": "3 John", + "3 Joh": "3 John", + "3Jn": "3 John", + "Jud": "Jude", + "Rev": "Revelation", + "Re": "Revelation", +} + + +def normalize_book_name(book: str) -> Optional[str]: + """ + Normalize book name variations to canonical form. + Returns the canonical book name if a variation is detected, None otherwise. + """ + return BOOK_ABBREVIATIONS.get(book) + + +def is_old_testament(book: str) -> bool: + """Check if a book is in the Old Testament.""" + canonical = normalize_book_name(book) or book + return canonical in OT_BOOKS + + +def is_new_testament(book: str) -> bool: + """Check if a book is in the New Testament.""" + canonical = normalize_book_name(book) or book + return canonical in NT_BOOKS + + +def get_testament(book: str) -> str: + """Get the testament for a book.""" + if is_old_testament(book): + return "Old Testament" + return "New Testament" diff --git a/kjvstudy_org/utils/helpers.py b/kjvstudy_org/utils/helpers.py new file mode 100644 index 0000000..9febbfb --- /dev/null +++ b/kjvstudy_org/utils/helpers.py @@ -0,0 +1,426 @@ +"""General helper functions for KJV Study.""" +import re +import hashlib +from datetime import datetime +from typing import Dict, Optional, List + +from ..kjv import bible, VerseReference +from ..topics import get_all_topics +from .books import normalize_book_name + + +def create_slug(text: str) -> str: + """Convert text to URL-friendly slug.""" + slug = re.sub(r'[^\w\s-]', '', text.lower()) + slug = re.sub(r'[-\s]+', '-', slug) + return slug.strip('-') + + +def get_verse_text(book: str, chapter: int, verse: int) -> str: + """Get the actual text of a specific verse.""" + try: + text = bible.get_verse_text(book, chapter, verse) + if text: + return text + return f"{book} {chapter}:{verse} text not found" + except Exception: + return f"{book} {chapter}:{verse}" + + +def is_verse_reference(query: str) -> bool: + """Check if query looks like a verse reference.""" + verse_pattern = r'^(I{1,3}|1|2|3)?\s*[A-Za-z]+(\s+[A-Za-z]+)?\s+\d+:\d+$' + return bool(re.match(verse_pattern, query.strip())) + + +def parse_verse_reference(query: str) -> Optional[Dict]: + """Parse a verse reference string and return verse info if found.""" + try: + cleaned_query = query.strip() + verse_ref = VerseReference.from_string(cleaned_query) + verse_text = bible.get_verse_text(verse_ref.book, verse_ref.chapter, verse_ref.verse) + + if verse_text: + return { + "book": verse_ref.book, + "chapter": verse_ref.chapter, + "verse": verse_ref.verse, + "text": verse_text, + "reference": f"{verse_ref.book} {verse_ref.chapter}:{verse_ref.verse}", + "url": f"/book/{verse_ref.book}/chapter/{verse_ref.chapter}#verse-{verse_ref.verse}", + "score": 100.0, + "highlighted_text": verse_text + } + + except Exception as e: + print(f"Error parsing verse reference '{query}': {e}") + + # Try alternative book name formats (Roman numerals to numbers) + try: + alternative_query = query.strip() + alternative_query = re.sub(r'^I\s+', '1 ', alternative_query) + alternative_query = re.sub(r'^II\s+', '2 ', alternative_query) + alternative_query = re.sub(r'^III\s+', '3 ', alternative_query) + + if alternative_query != query.strip(): + verse_ref = VerseReference.from_string(alternative_query) + verse_text = bible.get_verse_text(verse_ref.book, verse_ref.chapter, verse_ref.verse) + + if verse_text: + return { + "book": verse_ref.book, + "chapter": verse_ref.chapter, + "verse": verse_ref.verse, + "text": verse_text, + "reference": f"{verse_ref.book} {verse_ref.chapter}:{verse_ref.verse}", + "url": f"/book/{verse_ref.book}/chapter/{verse_ref.chapter}#verse-{verse_ref.verse}", + "score": 100.0, + "highlighted_text": verse_text + } + except Exception as e2: + print(f"Alternative parsing also failed for '{query}': {e2}") + + return None + + +def get_related_content(book: str, chapter: int = None, verse: int = None) -> Dict: + """Get related study guides, topics, and resources for a given passage.""" + related = { + "study_guides": [], + "topics": [], + "people": [], + "resources": [] + } + + # Map books to related people + book_people_map = { + "Genesis": [{"name": "Abraham", "url": "/family-tree"}, {"name": "Jacob", "url": "/family-tree"}], + "Exodus": [{"name": "Moses", "url": "/biblical-prophets/moses"}], + "1 Samuel": [{"name": "Samuel", "url": "/biblical-prophets"}], + "2 Samuel": [{"name": "David", "url": "/family-tree"}], + "1 Kings": [{"name": "Elijah", "url": "/biblical-prophets/elijah"}], + "2 Kings": [{"name": "Elijah", "url": "/biblical-prophets/elijah"}, {"name": "Elisha", "url": "/biblical-prophets"}], + "Isaiah": [{"name": "Isaiah", "url": "/biblical-prophets/isaiah"}], + "Jeremiah": [{"name": "Jeremiah", "url": "/biblical-prophets/jeremiah"}], + "Ezekiel": [{"name": "Ezekiel", "url": "/biblical-prophets/ezekiel"}], + "Daniel": [{"name": "Daniel", "url": "/biblical-prophets/daniel"}], + "Jonah": [{"name": "Jonah", "url": "/biblical-prophets/jonah"}], + "Matthew": [{"name": "The Twelve Apostles", "url": "/the-twelve-apostles"}], + "Mark": [{"name": "The Twelve Apostles", "url": "/the-twelve-apostles"}], + "Luke": [{"name": "The Twelve Apostles", "url": "/the-twelve-apostles"}, {"name": "John the Baptist", "url": "/biblical-prophets/john-the-baptist"}], + "John": [{"name": "John", "url": "/the-twelve-apostles/john"}], + "Acts": [{"name": "Peter", "url": "/the-twelve-apostles/peter"}, {"name": "Paul", "url": "/the-twelve-apostles"}], + "Ruth": [{"name": "Ruth", "url": "/women-of-the-bible/ruth"}], + "Esther": [{"name": "Esther", "url": "/women-of-the-bible/esther"}], + } + + if book in book_people_map: + related["people"] = book_people_map[book] + + # Map books/passages to special resources + if book in ["Exodus", "Leviticus", "Numbers", "Deuteronomy"]: + related["resources"].append({"name": "Biblical Festivals", "url": "/biblical-festivals"}) + related["resources"].append({"name": "Biblical Covenants", "url": "/biblical-covenants"}) + + if book in ["Genesis", "Exodus", "Numbers"]: + related["resources"].append({"name": "Biblical Timeline", "url": "/biblical-timeline"}) + + if book in ["Joshua", "Judges", "1 Samuel", "2 Samuel", "1 Kings", "2 Kings"]: + related["resources"].append({"name": "Biblical Maps", "url": "/biblical-maps"}) + + if book in ["Matthew", "Mark", "Luke", "John"]: + related["resources"].append({"name": "Parables of Jesus", "url": "/parables"}) + + # Add topic links based on common themes + topic_keywords = { + "Salvation": ["John", "Romans", "Ephesians", "Titus"], + "Prayer": ["Matthew", "Luke", "1 Thessalonians", "James"], + "Love": ["John", "1 Corinthians", "1 John"], + "Faith": ["Hebrews", "James", "Romans"], + "Hope": ["Romans", "1 Peter", "Hebrews"], + "Peace": ["Philippians", "John", "Romans"], + "Wisdom": ["Proverbs", "Ecclesiastes", "James"], + } + + topics_data = get_all_topics() + for topic_name in topics_data.keys(): + if topic_name in topic_keywords and book in topic_keywords[topic_name]: + related["topics"].append({"name": topic_name, "url": f"/topics/{topic_name}"}) + + return related + + +def get_chapter_popularity_score(book: str, chapter: int) -> int: + """Calculate popularity score for a chapter (1-10 scale) based on well-known verses.""" + popular_chapters = { + "John": {3: 10}, + "1 Corinthians": {13: 10}, + "Psalms": {23: 10, 91: 9, 1: 8, 139: 8}, + "Romans": {8: 9, 3: 8, 12: 8}, + "Matthew": {5: 9, 6: 8, 7: 8}, + "Ephesians": {2: 8, 6: 8}, + "Philippians": {4: 8}, + "Genesis": {1: 9, 3: 8, 22: 7}, + "Exodus": {20: 8, 14: 7}, + "Isaiah": {53: 9, 40: 8}, + "Jeremiah": {29: 7}, + "Proverbs": {31: 7, 3: 7}, + "Ecclesiastes": {3: 8}, + "1 Peter": {5: 7}, + "James": {1: 7}, + "Hebrews": {11: 8, 12: 7}, + "Revelation": {21: 8, 22: 7}, + "Luke": {2: 9, 15: 8}, + "2 Timothy": {3: 7}, + "Joshua": {1: 7}, + "Daniel": {3: 7, 6: 7}, + "1 John": {4: 8}, + "Galatians": {5: 7}, + "Colossians": {3: 7}, + "1 Thessalonians": {4: 7}, + "Mark": {16: 7}, + "Acts": {2: 8}, + "1 Samuel": {17: 7}, + "Job": {19: 7}, + "2 Corinthians": {5: 7}, + "1 Kings": {3: 6, 18: 6}, + "Malachi": {3: 6}, + "Joel": {2: 6}, + "Micah": {6: 6}, + "Habakkuk": {2: 6}, + } + + if book in popular_chapters and chapter in popular_chapters[book]: + return popular_chapters[book][chapter] + + default_score = 4 + if chapter == 1: + default_score += 1 + + high_readership_books = [ + "Matthew", "Mark", "Luke", "John", "Acts", "Romans", + "1 Corinthians", "2 Corinthians", "Galatians", "Ephesians", + "Philippians", "Colossians", "Genesis", "Exodus", "Psalms", "Proverbs" + ] + if book in high_readership_books: + default_score += 1 + + total_chapters = len([ch for bk, ch in bible.iter_chapters() if bk == book]) + if total_chapters <= 5: + default_score += 1 + + return min(default_score, 6) + + +def get_chapter_popularity_explanation(book: str, chapter: int) -> str: + """Get explanation for why a chapter is popular or what it contains.""" + explanations = { + "John": { + 3: "Contains John 3:16 - 'For God so loved the world' - the most quoted verse in Christianity", + 1: "The Word became flesh - Jesus as the eternal Logos and the calling of the first disciples", + }, + "1 Corinthians": { + 13: "The famous 'Love Chapter' - 'Love is patient, love is kind' - essential reading for weddings and Christian living", + }, + "Psalms": { + 23: "The beloved Shepherd Psalm - 'The Lord is my shepherd, I shall not want' - comfort in times of trouble", + 91: "Psalm of protection - 'He who dwells in the shelter of the Most High' - promises of God's care", + 1: "The blessed man - contrasts the righteous and wicked, foundation of wisdom literature", + 139: "God's omniscience and omnipresence - 'You have searched me and known me' - intimate knowledge of God", + }, + "Romans": { + 8: "No condemnation in Christ - 'All things work together for good' - assurance of salvation", + 3: "All have sinned - universal need for salvation and justification by faith", + 12: "Living sacrifice - practical Christian living and spiritual gifts", + }, + "Matthew": { + 5: "The Beatitudes - 'Blessed are the poor in spirit' - foundation of Christian ethics", + 6: "The Lord's Prayer and teachings on worry - 'Give us this day our daily bread'", + 7: "Golden Rule and narrow gate - 'Do unto others as you would have them do unto you'", + }, + "Ephesians": { + 2: "Salvation by grace through faith - 'not by works' - core Protestant doctrine", + 6: "Armor of God - spiritual warfare and family relationships", + }, + "Philippians": { + 4: "Joy and peace in Christ - 'I can do all things through Christ' and 'Be anxious for nothing'", + }, + "Genesis": { + 1: "Creation account - 'In the beginning God created the heavens and the earth'", + 3: "The Fall - Adam and Eve's disobedience and the first promise of redemption", + 22: "Abraham's ultimate test - the near-sacrifice of Isaac, foreshadowing Christ", + }, + "Exodus": { + 20: "The Ten Commandments - moral foundation given to Moses on Mount Sinai", + 14: "Crossing the Red Sea - God's miraculous deliverance of Israel from Egypt", + }, + "Isaiah": { + 53: "The Suffering Servant - 'He was wounded for our transgressions' - prophecy of Christ's crucifixion", + 40: "Comfort my people - 'Every valley shall be exalted' - hope and restoration", + }, + "Jeremiah": { + 29: "'I know the plans I have for you' - God's promises during exile, hope for the future", + }, + "Proverbs": { + 31: "The virtuous woman - 'Her price is far above rubies' - ideal of godly womanhood", + 3: "'Trust in the Lord with all your heart' - foundational wisdom for life", + }, + "Ecclesiastes": { + 3: "'To everything there is a season' - the famous passage on time and purpose", + }, + "1 Peter": { + 5: "'Cast all your anxiety on him' - comfort for suffering Christians", + }, + "James": { + 1: "Faith and trials - 'Count it all joy when you fall into various trials'", + }, + "Hebrews": { + 11: "Hall of Faith - examples of faithful men and women throughout history", + 12: "'Let us run with endurance the race set before us' - perseverance in faith", + }, + "Revelation": { + 21: "New heaven and new earth - 'God will wipe away every tear' - ultimate hope", + 22: "The final invitation - 'Come, Lord Jesus' - conclusion of Scripture", + }, + "Luke": { + 2: "The Christmas story - birth of Jesus, shepherds, and Mary's pondering heart", + 15: "Lost sheep, lost coin, and prodigal son - parables of God's pursuing love", + }, + "2 Timothy": { + 3: "'All Scripture is given by inspiration of God' - doctrine of biblical inspiration", + }, + "Joshua": { + 1: "'Be strong and of good courage' - God's commissioning of Joshua as leader", + }, + "Daniel": { + 3: "Shadrach, Meshach, and Abednego in the fiery furnace - faith under persecution", + 6: "Daniel in the lion's den - integrity and God's deliverance", + }, + "1 John": { + 4: "'God is love' - the essential nature of God and perfect love casting out fear", + }, + "Galatians": { + 5: "Fruits of the Spirit - 'love, joy, peace, patience' - Christian character", + }, + "Colossians": { + 3: "'Set your mind on things above' - heavenly perspective on earthly life", + }, + "1 Thessalonians": { + 4: "The rapture - 'We shall be caught up together' - Second Coming of Christ", + }, + "Mark": { + 16: "The Great Commission - 'Go into all the world and preach the gospel'", + }, + "Acts": { + 2: "Pentecost - the Holy Spirit comes and the church is born", + }, + "1 Samuel": { + 17: "David and Goliath - faith triumphs over impossible odds", + }, + "Job": { + 19: "'I know that my Redeemer lives' - hope in the midst of suffering", + }, + "2 Corinthians": { + 5: "'If anyone is in Christ, he is a new creation' - transformation in Christ", + }, + "1 Kings": { + 3: "Solomon's wisdom - asking for an understanding heart to judge God's people", + 18: "Elijah and the prophets of Baal - 'The Lord, He is God!'", + }, + "Malachi": { + 3: "Tithing and God's faithfulness - 'Bring all the tithes into the storehouse'", + }, + "Joel": { + 2: "'I will pour out My Spirit on all flesh' - prophecy of the Spirit's outpouring", + }, + "Micah": { + 6: "'What does the Lord require of you?' - justice, mercy, and humble walking with God", + }, + "Habakkuk": { + 2: "'The just shall live by faith' - foundational verse for Protestant Reformation", + }, + } + + if book in explanations and chapter in explanations[book]: + return explanations[book][chapter] + + if chapter == 1: + return f"Opening chapter of {book} - introduces key themes and characters" + + if book in ["Matthew", "Mark", "Luke", "John"]: + return "Gospel account of Jesus' life and ministry" + elif book in ["Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy"]: + return "Torah/Pentateuch - foundational law and history of Israel" + elif book in ["Psalms", "Proverbs", "Ecclesiastes", "Song of Solomon"]: + return "Wisdom literature - poetry and practical life guidance" + elif book in ["Isaiah", "Jeremiah", "Ezekiel", "Daniel"]: + return "Major prophet - messages of judgment and hope" + elif book in ["Romans", "1 Corinthians", "2 Corinthians", "Galatians", "Ephesians", + "Philippians", "Colossians", "1 Thessalonians", "2 Thessalonians", + "1 Timothy", "2 Timothy", "Titus", "Philemon"]: + return "Pauline epistle - apostolic teaching for the early church" + elif book == "Acts": + return "History of the early church and spread of the gospel" + elif book == "Revelation": + return "Apocalyptic vision of the end times and Christ's victory" + else: + return f"Part of {book} - explore this chapter to discover its significance" + + +# Featured verses for verse of the day +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), +] + + +def get_daily_verse(date_str: str = None) -> Dict: + """Get the verse of the day based on a specific date (or current date if not provided).""" + if date_str is None: + date_str = datetime.now().strftime("%Y-%m-%d") + seed = int(hashlib.md5(date_str.encode()).hexdigest(), 16) % 1000000 + + 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: + 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 + } diff --git a/kjvstudy_org/utils/search.py b/kjvstudy_org/utils/search.py new file mode 100644 index 0000000..05c33f8 --- /dev/null +++ b/kjvstudy_org/utils/search.py @@ -0,0 +1,73 @@ +"""Search functionality for Bible verses.""" +from typing import List, Dict, Optional + +from ..kjv import bible +from .helpers import is_verse_reference, parse_verse_reference + + +def perform_full_text_search(query: str, limit: Optional[int] = None) -> List[Dict]: + """Perform full text search across all Bible verses or find specific verse references.""" + results = [] + + # First, check if this looks like a verse reference + if is_verse_reference(query): + verse_result = parse_verse_reference(query) + if verse_result: + return [verse_result] + + # If not a verse reference or verse not found, perform regular text search + search_terms = query.lower().split() + + # Search through all verses using the iter_verses method + for verse in bible.iter_verses(): + verse_text = verse.text.lower() + + # Check if all search terms are in the verse + if all(term in verse_text for term in search_terms): + # Calculate relevance score + score = calculate_relevance_score(verse.text, search_terms) + + results.append({ + "book": verse.book, + "chapter": verse.chapter, + "verse": verse.verse, + "text": verse.text, + "reference": f"{verse.book} {verse.chapter}:{verse.verse}", + "url": f"/book/{verse.book}/chapter/{verse.chapter}#verse-{verse.verse}", + "score": score, + "highlighted_text": highlight_search_terms(verse.text, search_terms) + }) + + # Sort by relevance score (higher is better) + results.sort(key=lambda x: x["score"], reverse=True) + + # Limit results if specified + if limit is not None: + return results[:limit] + return results + + +def calculate_relevance_score(text: str, search_terms: List[str]) -> float: + """Calculate relevance score for search results.""" + text_lower = text.lower() + score = 0.0 + + for term in search_terms: + # Count occurrences of each term + count = text_lower.count(term.lower()) + score += count + + # Bonus for exact word matches + if f" {term.lower()} " in f" {text_lower} ": + score += 0.5 + + return score + + +def highlight_search_terms(text: str, search_terms: List[str]) -> str: + """Highlight search terms in text.""" + highlighted = text + for term in search_terms: + # Simple highlighting (could be improved) + highlighted = highlighted.replace(term, f"{term}") + return highlighted