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:
2025-11-24 17:42:42 -05:00
parent d694c822ee
commit 0dd02c6bf9
8 changed files with 1296 additions and 19 deletions
+6
View File
@@ -0,0 +1,6 @@
# Route modules for KJV Study
from fastapi import APIRouter
from .api import router as api_router
__all__ = ['api_router']
+442
View File
@@ -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
View File
@@ -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)}.
+33 -11
View File
@@ -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 %}
+8
View File
@@ -0,0 +1,8 @@
# Utility modules for KJV Study
# Note: Import individual modules to avoid circular imports
__all__ = [
'books',
'search',
'helpers',
]
+246
View File
@@ -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"
+426
View File
@@ -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
}
+73
View File
@@ -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