mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-10-24 14:29:14 +00:00
251 lines
8.1 KiB
Python
251 lines
8.1 KiB
Python
"""Passage study workspace routes."""
|
|
import re
|
|
from urllib.parse import quote
|
|
|
|
from ..kjv import bible
|
|
from ..cross_references import get_cross_references
|
|
from ..interlinear_loader import get_interlinear_data
|
|
from ..commentary_gen import (
|
|
generate_chapter_overview,
|
|
generate_commentary,
|
|
generate_word_study_sidenotes,
|
|
)
|
|
from ..utils.books import normalize_book_name
|
|
from ..utils.helpers import get_related_content
|
|
from ._helpers import abort_404, redirect, render
|
|
from .bible import group_cross_references, _CROSS_REF_NOTE_PRIORITY
|
|
|
|
|
|
_REFERENCE_PATTERNS = (
|
|
re.compile(r"^(.+?)\s+(\d+):(\d+)(?:-(\d+))?$"),
|
|
re.compile(r"^(.+?)\s+(\d+)\s+(\d+)(?:-(\d+))?$"),
|
|
re.compile(r"^(.+?)\s+(\d+)$"),
|
|
)
|
|
|
|
|
|
def _canonical_book(book: str):
|
|
candidate = normalize_book_name(book) or book
|
|
return candidate if candidate in bible.get_books() else None
|
|
|
|
|
|
def parse_study_reference(reference: str):
|
|
"""Parse a chapter or passage reference for the study workspace."""
|
|
cleaned = " ".join((reference or "").strip().split())
|
|
if not cleaned:
|
|
return None
|
|
|
|
for pattern in _REFERENCE_PATTERNS:
|
|
match = pattern.match(cleaned)
|
|
if not match:
|
|
continue
|
|
|
|
book = _canonical_book(match.group(1))
|
|
if not book:
|
|
return None
|
|
|
|
chapter = int(match.group(2))
|
|
verse_start = (
|
|
int(match.group(3))
|
|
if match.lastindex and match.lastindex >= 3 and match.group(3)
|
|
else None
|
|
)
|
|
verse_end = (
|
|
int(match.group(4))
|
|
if match.lastindex and match.lastindex >= 4 and match.group(4)
|
|
else None
|
|
)
|
|
|
|
if verse_start is not None and verse_end is not None and verse_start > verse_end:
|
|
return None
|
|
|
|
return book, chapter, verse_start, verse_end
|
|
|
|
return None
|
|
|
|
|
|
def _study_url(book: str, chapter: int, verse_start=None, verse_end=None):
|
|
book_part = quote(book)
|
|
if verse_start is None:
|
|
return f"/study/{book_part}/{chapter}"
|
|
if verse_end is None or verse_end == verse_start:
|
|
return f"/study/{book_part}/{chapter}/{verse_start}"
|
|
return f"/study/{book_part}/{chapter}/{verse_start}/{verse_end}"
|
|
|
|
|
|
def _passage_label(book: str, chapter: int, verse_start=None, verse_end=None):
|
|
if verse_start is None:
|
|
return f"{book} {chapter}"
|
|
if verse_end is None or verse_end == verse_start:
|
|
return f"{book} {chapter}:{verse_start}"
|
|
return f"{book} {chapter}:{verse_start}-{verse_end}"
|
|
|
|
|
|
def _build_study_rows(book: str, chapter: int, verses):
|
|
rows = []
|
|
recent_words = {}
|
|
seen_words = set()
|
|
cooldown_verses = 5
|
|
|
|
for verse in verses:
|
|
commentary = generate_commentary(book, chapter, verse)
|
|
|
|
excluded_words = {
|
|
word
|
|
for word, previous_verse in recent_words.items()
|
|
if verse.verse - previous_verse < cooldown_verses
|
|
}
|
|
word_studies = generate_word_study_sidenotes(
|
|
verse.text, book, chapter, verse.verse, excluded_words
|
|
)
|
|
for study in word_studies:
|
|
word_lower = study["word"].lower()
|
|
study["auto_expand"] = word_lower not in seen_words
|
|
seen_words.add(word_lower)
|
|
recent_words[word_lower] = verse.verse
|
|
|
|
cross_refs = get_cross_references(book, chapter, verse.verse)
|
|
grouped_refs = group_cross_references(cross_refs, book, chapter, build_urls=True)
|
|
sorted_groups = sorted(
|
|
grouped_refs.items(),
|
|
key=lambda item: _CROSS_REF_NOTE_PRIORITY.get(item[0], 4),
|
|
)
|
|
|
|
interlinear_words = get_interlinear_data(book, chapter, verse.verse) or []
|
|
has_commentary = bool(
|
|
commentary
|
|
and (
|
|
commentary.get("analysis")
|
|
or commentary.get("historical")
|
|
or commentary.get("theological")
|
|
or commentary.get("questions")
|
|
)
|
|
)
|
|
|
|
rows.append({
|
|
"verse": verse,
|
|
"reference": f"{book} {chapter}:{verse.verse}",
|
|
"commentary": commentary,
|
|
"has_commentary": has_commentary,
|
|
"word_studies": word_studies,
|
|
"cross_reference_groups": [
|
|
{"description": desc, "refs": refs}
|
|
for desc, refs in sorted_groups
|
|
],
|
|
"interlinear_preview": interlinear_words[:12],
|
|
"interlinear_count": len(interlinear_words),
|
|
})
|
|
|
|
return rows
|
|
|
|
|
|
def _render_landing(req, resp, *, query="", error=None):
|
|
render(
|
|
req, resp, "study_workspace.html",
|
|
books=bible.get_books(),
|
|
breadcrumbs=[{"text": "Home", "url": "/"}, {"text": "Study", "url": None}],
|
|
query=query,
|
|
error=error,
|
|
workspace=None,
|
|
)
|
|
|
|
|
|
def _render_workspace(req, resp, *, book, chapter, verse_start=None, verse_end=None):
|
|
canonical = _canonical_book(book)
|
|
if canonical and canonical != book:
|
|
redirect(resp, _study_url(canonical, chapter, verse_start, verse_end), 301)
|
|
return
|
|
|
|
chapters = bible.get_chapters_for_book(book)
|
|
if not chapters:
|
|
abort_404(req, resp, f"The book '{book}' was not found.")
|
|
return
|
|
|
|
chapter_verses = bible.get_verses_by_book_chapter(book, chapter)
|
|
if not chapter_verses:
|
|
abort_404(req, resp, f"Chapter {chapter} of {book} was not found.")
|
|
return
|
|
|
|
if verse_start is None:
|
|
selected_verses = chapter_verses
|
|
verse_end = None
|
|
else:
|
|
if verse_end is None:
|
|
verse_end = verse_start
|
|
if verse_start > verse_end:
|
|
abort_404(req, resp, "The requested passage range was not found.")
|
|
return
|
|
selected_verses = [
|
|
verse for verse in chapter_verses
|
|
if verse_start <= verse.verse <= verse_end
|
|
]
|
|
|
|
if not selected_verses:
|
|
abort_404(req, resp, "The requested passage range was not found.")
|
|
return
|
|
|
|
label = _passage_label(book, chapter, verse_start, verse_end)
|
|
rows = _build_study_rows(book, chapter, selected_verses)
|
|
|
|
workspace = {
|
|
"book": book,
|
|
"chapter": chapter,
|
|
"verse_start": verse_start,
|
|
"verse_end": verse_end,
|
|
"label": label,
|
|
"url": _study_url(book, chapter, verse_start, verse_end),
|
|
"chapter_url": f"/book/{quote(book)}/chapter/{chapter}",
|
|
"interlinear_url": f"/book/{quote(book)}/chapter/{chapter}/interlinear",
|
|
"rows": rows,
|
|
"verse_count": len(selected_verses),
|
|
"chapter_overview": generate_chapter_overview(book, chapter, chapter_verses),
|
|
"related_content": get_related_content(book, chapter),
|
|
}
|
|
|
|
render(
|
|
req, resp, "study_workspace.html",
|
|
books=bible.get_books(),
|
|
breadcrumbs=[
|
|
{"text": "Home", "url": "/"},
|
|
{"text": "Study", "url": "/study"},
|
|
{"text": label, "url": None},
|
|
],
|
|
query=label,
|
|
error=None,
|
|
workspace=workspace,
|
|
)
|
|
|
|
|
|
def register(api):
|
|
@api.route("/study")
|
|
async def study_landing(req, resp):
|
|
query = (req.params.get("q") or "").strip()
|
|
if query:
|
|
parsed = parse_study_reference(query)
|
|
if parsed:
|
|
book, chapter, verse_start, verse_end = parsed
|
|
redirect(resp, _study_url(book, chapter, verse_start, verse_end), 302)
|
|
return
|
|
_render_landing(req, resp, query=query, error="Reference not found")
|
|
return
|
|
|
|
_render_landing(req, resp)
|
|
|
|
@api.route("/study/{book}/{chapter:int}")
|
|
async def study_chapter(req, resp, *, book, chapter):
|
|
_render_workspace(req, resp, book=book, chapter=chapter)
|
|
|
|
@api.route("/study/{book}/{chapter:int}/{verse_start:int}")
|
|
async def study_single_verse(req, resp, *, book, chapter, verse_start):
|
|
_render_workspace(
|
|
req, resp,
|
|
book=book, chapter=chapter, verse_start=verse_start
|
|
)
|
|
|
|
@api.route("/study/{book}/{chapter:int}/{verse_start:int}/{verse_end:int}")
|
|
async def study_passage(req, resp, *, book, chapter, verse_start, verse_end):
|
|
_render_workspace(
|
|
req, resp,
|
|
book=book, chapter=chapter,
|
|
verse_start=verse_start, verse_end=verse_end
|
|
)
|