mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 23:00:16 +00:00
Add modular code structure with utils and routes packages
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Route modules for KJV Study
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .api import router as api_router
|
||||
|
||||
__all__ = ['api_router']
|
||||
@@ -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
|
||||
+62
-8
@@ -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'<label for="sn-{verse_num}-word-{idx}" class="margin-toggle sidenote-number"></label><input type="checkbox" id="sn-{verse_num}-word-{idx}" class="margin-toggle"/><span class="sidenote"><strong>{word}:</strong> {study["term"]} (<em>{study["translit"]}</em>). {study["note"]}</span>'
|
||||
|
||||
# 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'<a href="#verse-{start}">Verse {start}</a>'
|
||||
else:
|
||||
return f'<a href="#verse-{start}-{end}">Verses {start}-{end}</a>'
|
||||
|
||||
overview = f"""
|
||||
<p><strong>{book} {chapter}</strong> 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}</p>
|
||||
@@ -9729,10 +9776,10 @@ def generate_chapter_overview(book, chapter, verses):
|
||||
<p>The chapter can be divided into several sections:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Verses 1-{min(5, len(verses))}</strong>: Introduction and setting the context</li>
|
||||
{'<li><strong>Verses 6-' + str(min(12, len(verses))) + '</strong>: Development of key themes</li>' if len(verses) > 5 else ''}
|
||||
{'<li><strong>Verses 13-' + str(min(20, len(verses))) + '</strong>: Central message and teachings</li>' if len(verses) > 12 else ''}
|
||||
{'<li><strong>Verses ' + str(min(21, len(verses))) + '-' + str(len(verses)) + '</strong>: Conclusion and application</li>' if len(verses) > 20 else ''}
|
||||
<li><strong>{verse_link(1, min(5, len(verses)))}</strong>: Introduction and setting the context</li>
|
||||
{'<li><strong>' + verse_link(6, min(12, len(verses))) + '</strong>: Development of key themes</li>' if len(verses) > 5 else ''}
|
||||
{'<li><strong>' + verse_link(13, min(20, len(verses))) + '</strong>: Central message and teachings</li>' if len(verses) > 12 else ''}
|
||||
{'<li><strong>' + verse_link(min(21, len(verses)), len(verses)) + '</strong>: Conclusion and application</li>' if len(verses) > 20 else ''}
|
||||
</ol>
|
||||
|
||||
<p>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'<a href="#verse-{start}">Verse {start}</a>'
|
||||
else:
|
||||
return f'<a href="#verse-{start}-{end}">Verses {start}-{end}</a>'
|
||||
|
||||
overview = f"""
|
||||
<p><strong>{book} {chapter}</strong> 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}</p>
|
||||
@@ -10098,10 +10152,10 @@ def generate_chapter_overview(book, chapter, verses):
|
||||
<p>The chapter can be divided into several sections:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Verses 1-{min(5, len(verses))}</strong>: Introduction and setting the context</li>
|
||||
{'<li><strong>Verses 6-' + str(min(12, len(verses))) + '</strong>: Development of key themes</li>' if len(verses) > 5 else ''}
|
||||
{'<li><strong>Verses 13-' + str(min(20, len(verses))) + '</strong>: Central message and teachings</li>' if len(verses) > 12 else ''}
|
||||
{'<li><strong>Verses ' + str(min(21, len(verses))) + '-' + str(len(verses)) + '</strong>: Conclusion and application</li>' if len(verses) > 20 else ''}
|
||||
<li><strong>{verse_link(1, min(5, len(verses)))}</strong>: Introduction and setting the context</li>
|
||||
{'<li><strong>' + verse_link(6, min(12, len(verses))) + '</strong>: Development of key themes</li>' if len(verses) > 5 else ''}
|
||||
{'<li><strong>' + verse_link(13, min(20, len(verses))) + '</strong>: Central message and teachings</li>' if len(verses) > 12 else ''}
|
||||
{'<li><strong>' + verse_link(min(21, len(verses)), len(verses)) + '</strong>: Conclusion and application</li>' if len(verses) > 20 else ''}
|
||||
</ol>
|
||||
|
||||
<p>This chapter is significant because it {get_chapter_significance(book, chapter)}.
|
||||
|
||||
@@ -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 %}
|
||||
<p id="verse-{{ verse.verse }}">
|
||||
<a href="/book/{{ book }}/chapter/{{ chapter }}/verse/{{ verse.verse }}" class="verse-number-link">{{ verse.verse }}</a> {{ verse.text | link_names | safe }}
|
||||
<a href="/book/{{ book }}/chapter/{{ chapter }}/verse/{{ verse.verse }}" class="verse-number-link">{{ verse.verse }}</a> {{ 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 %}
|
||||
<label for="sn-{{ verse.verse }}-word-{{ loop.index }}" class="margin-toggle sidenote-number"></label>
|
||||
<input type="checkbox" id="sn-{{ verse.verse }}-word-{{ loop.index }}" class="margin-toggle"/>
|
||||
<span class="sidenote"><strong>{{ study.word }}:</strong> {{ study.term }} (<em>{{ study.translit }}</em>). {{ study.note | safe }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if commentary.cross_references %}
|
||||
{% for ref in commentary.cross_references %}
|
||||
<label for="sn-{{ verse.verse }}-ref-{{ loop.index }}" class="margin-toggle">⊕</label>
|
||||
<label for="sn-{{ verse.verse }}-ref-{{ loop.index }}" class="margin-toggle sidenote-number"></label>
|
||||
<input type="checkbox" id="sn-{{ verse.verse }}-ref-{{ loop.index }}" class="margin-toggle"/>
|
||||
<span class="marginnote"><strong><a href="{{ ref.url }}">{{ ref.text }}</a></strong>{% if ref.context %} — {{ ref.context }}{% endif %}</span>
|
||||
<span class="sidenote"><strong><a href="{{ ref.url }}">{{ ref.text }}</a></strong>{% if ref.context %} — {{ ref.context }}{% endif %}</span>
|
||||
{% 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Utility modules for KJV Study
|
||||
# Note: Import individual modules to avoid circular imports
|
||||
|
||||
__all__ = [
|
||||
'books',
|
||||
'search',
|
||||
'helpers',
|
||||
]
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"<mark>{term}</mark>")
|
||||
return highlighted
|
||||
Reference in New Issue
Block a user