diff --git a/app.py b/app.py index 111b837..7a29a2f 100644 --- a/app.py +++ b/app.py @@ -19,6 +19,12 @@ import sys import logging import socket import json +import hashlib +import re +import difflib +import math +import urllib.parse +import html as html_lib from tools import sanity_check try: from dotenv import load_dotenv @@ -75,6 +81,277 @@ def dict_factory(cursor, row): kjv_cur = None sermon_cur = None +# --------------------------------------------------------------------------- +# Verse menu helpers (favorites, definitions and cross references are saved by +# the browser itself; the server only supplies the lookups) +# --------------------------------------------------------------------------- + +DICT_STOPWORDS = { + "the", "and", "that", "for", "him", "his", "her", "but", "not", "with", "are", + "was", "were", "unto", "thou", "thee", "thy", "ye", "you", "all", "from", "they", + "them", "this", "have", "had", "shall", "will", "which", "into", "upon", "then", +} + + +def format_verse_ref(verse_id) -> str: + """12345678 style id -> 'John 3:16'""" + verse_id = int(verse_id) + return f"{lookup_bookname(verse_id // 1_000_000)} {verse_id // 1000 % 1000}:{verse_id % 1000}" + + +@app.get("/xref/") # cross references for one verse, best ranked first +def cross_references(verse_id: int): + rows = kjv_cur.execute( + "SELECT sv, ev FROM cross_reference WHERE vid = ? ORDER BY r DESC LIMIT 15", + (verse_id,), + ).fetchall() + results = [] + for start_id, end_id in rows: + ref = format_verse_ref(start_id) + if end_id and end_id > start_id: + end_ref = format_verse_ref(end_id) + # "John 3:16-18" when the range stays in one chapter + same_chapter = start_id // 1000 == end_id // 1000 + ref += "-" + (str(end_id % 1000) if same_chapter else end_ref) + verses = lookup_by_verse_id(start_id, end_id) + else: + verses = lookup_by_verse_id(start_id) + results.append({ + "ref": ref, + "verse_id": start_id, + "text": " ".join(v[4] for v in verses), + }) + return jsonify(results) + + +@app.get("/defs") # 1828 Webster definitions for several words at once: /defs?words=grace,loved +def definitions(): + words = request.args.get("words", "").lower().replace(",", " ").split()[:40] + dictionary = cached["json_dictionary"] + found = {} + for word in words: + word = "".join(c for c in word if c.isalpha()) + if word and word not in DICT_STOPWORDS and word in dictionary: + found[word] = dictionary[word] + return jsonify(found) + + +@app.get("/saved") # favorites, definitions and cross references kept in this browser +def saved_page(): + return static_page("html/saved.html") + + +# --------------------------------------------------------------------------- +# Bible reading plan: a /seq style tracker that can go backwards. The plan is +# its UID; the page URL (/plan/?n=) is what gets bookmarked. +# --------------------------------------------------------------------------- + +PLAN_UID_CHARS = "0123456789ACEFHJKLMNPRTUVWXY" +PLAN_MAX_PER_DAY = 500 + + +def plan_connection(): + if usermode == "Single": + conn = sqlite3.connect("data/seq.db", check_same_thread=False) + else: + conn = sqlite3.connect("data/seq.db") + conn.execute( + "CREATE TABLE IF NOT EXISTS reading_tracker (uid TEXT PRIMARY KEY, verse INTEGER, last_used TEXT)" + ) + return conn + + +def plan_per_day(raw, default=25) -> int: + try: + return max(1, min(PLAN_MAX_PER_DAY, int(raw))) + except (TypeError, ValueError): + return default + + +def plan_start_index(ref: str): + """Index into verse_ids for a reference like 'Matthew 1:1'; None if it can't be parsed.""" + ref = (ref or "").strip() + if not ref: + return 0 + try: + verse_id = int(show(kw=ref, get_start_verse_id=True)) + return cached["verse_ids"].index(verse_id) + except Exception: + return None + + +def plan_error(message: str): + return static_html_doc( + "Reading Plan", + f"

Reading Plan

{html_lib.escape(message)}

Back to plan setup

", + ), 400 + + +@app.get("/plan") # setup form for a new reading plan +def reading_plan(): + return static_page("html/plan.html") + + +@app.post("/plan/new") +def plan_create(): + total = len(cached["verse_ids"]) + start = plan_start_index(request.form.get("start", "")) + if start is None: + return plan_error("We couldn't find that starting point. Try something like 'Matthew 1:1'.") + if request.form.get("pace") == "verses": + per_day = plan_per_day(request.form.get("verses")) + else: + try: + days = max(1, min(3650, int(request.form.get("days", 365)))) + except ValueError: + days = 365 + per_day = plan_per_day(math.ceil((total - start) / days)) + conn = plan_connection() + for _ in range(5): # a clash on a random 5 character id is very unlikely + uid = generate_short_id() + try: + conn.execute( + "INSERT INTO reading_tracker VALUES(?, ?, ?)", (uid, start, str(datetime.now())) + ) + break + except sqlite3.IntegrityError: + continue + else: + return plan_error("Could not create a plan, please try again.") + conn.commit() + conn.close() + logging.info(f"New reading plan {uid} start={start} per_day={per_day}") + return redirect(f"/plan/{uid}?n={per_day}", code=303) + + +def plan_uid_or_none(raw: str): + uid = search_input_cleaning(raw, valid_chars=PLAN_UID_CHARS) + return uid if uid and uid == raw else None + + +@app.post("/plan//move") # back / next / jump; POST so a page prefetch can never move your place +def plan_move(uid: str): + uid = plan_uid_or_none(uid) + per_day = plan_per_day(request.form.get("n")) + if not uid: + return plan_error("That reading plan id is not valid.") + conn = plan_connection() + row = conn.execute("SELECT verse FROM reading_tracker WHERE uid = ?", (uid,)).fetchone() + if row is None: + conn.close() + return plan_error("That reading plan was not found (plans unused for 90 days are removed).") + total = len(cached["verse_ids"]) + position = row[0] + action = request.form.get("action") + if action == "next": + position = min(total, position + per_day) + elif action == "back": + position = max(0, position - per_day) + elif action == "restart": + position = 0 + elif action == "goto": + target = plan_start_index(request.form.get("kw", "")) + if target is None: + conn.close() + return plan_error("We couldn't find that reference. Try something like 'John 3:1'.") + position = target + conn.execute( + "REPLACE INTO reading_tracker (uid, verse, last_used) VALUES(?, ?, ?)", + (uid, position, str(datetime.now())), + ) + conn.commit() + conn.close() + return redirect(f"/plan/{uid}?n={per_day}", code=303) + + +@app.get("/plan/") # today's reading for a plan. Viewing never moves your place. +def plan_view(uid: str): + uid = plan_uid_or_none(uid) + if not uid: + return plan_error("That reading plan id is not valid.") + per_day = plan_per_day(request.args.get("n")) + conn = plan_connection() + row = conn.execute("SELECT verse FROM reading_tracker WHERE uid = ?", (uid,)).fetchone() + if row is None: + conn.close() + return plan_error("That reading plan was not found (plans unused for 90 days are removed).") + position = row[0] + # keeps an active plan from being purged as unused + conn.execute( + "REPLACE INTO reading_tracker (uid, verse, last_used) VALUES(?, ?, ?)", + (uid, position, str(datetime.now())), + ) + conn.commit() + conn.close() + + verse_ids = cached["verse_ids"] + total = len(verse_ids) + finished = position >= total + last_index = min(total, position + per_day) - 1 + percent = round(100 * min(position, total) / total, 1) + esc = html_lib.escape + + controls = f"""
+""" + back_disabled = "disabled" if position <= 0 else "" + next_html = ( + "" if finished else + "" + ) + controls += f"""{next_html} + +
+
+ + + +
+
+
""" + + if finished: + summary = ( + "
You've finished the plan!" + f"
You read all {total:,} verses. Use Previous reading to look back, or start over.
" + ) + verses_html = "" + controls += f"""
+ +
""" + else: + first_ref = format_verse_ref(verse_ids[position]) + last_ref = format_verse_ref(verse_ids[last_index]) + count = last_index - position + 1 + summary = ( + "
Today's reading" + f"
{esc(first_ref)} – {esc(last_ref)}
" + f"
{count} verses · {percent}% of the Bible complete
" + f"
" + ) + verses = lookup_by_verse_id(verse_ids[position], verse_ids[last_index]) + verses_html = "".join(response_rich(v) for v in verses) + + bookmark_note = ( + "
Bookmark this page — it is your reading plan. " + "Come back each day, then press Done to move on.
" + ) + store_script = ( + "" + ) + return ( + render_navbar("", bar_html="") + + "
" + + summary + + verses_html + + f"
{controls}
" + + bookmark_note + + store_script + + "
" + ) + + + if __name__ == "__main__": # single user mode, running without uwsgi usermode: str = "Single" @@ -117,6 +394,20 @@ for group in theological_synonym_groups: concept_mappings = json.load(open("data/concept_mappings.json", "r")) concept_mappings_dict = {mapping["query"].lower(): mapping for mapping in concept_mappings} +# Static assets are served with a very long cache lifetime, so their URLs carry a +# hash of their contents ("?v=...") and change whenever the files do. +ASSET_VERSION: str = hashlib.md5( + b"".join( + open(f, "rb").read() + for f in ("styles/page.css", "js/collapsible.js", "js/shortcode.js", "js/verse_menu.js", "js/book_suggest.js") + ) +).hexdigest()[:8] + + +def load_html(path: str) -> str: + return open(path, "r").read().replace("__V__", ASSET_VERSION) + + cached: dict = { "verse_ids": [i[0] for i in kjv_cur.execute("SELECT id FROM fts_kjv;").fetchall()], "book_names": [ @@ -129,19 +420,29 @@ cached: dict = { "theological_synonyms": theological_synonyms_map, "concept_mappings": concept_mappings_dict, "html_response": open("html/response.html", "r").read(), - "html_navbar": open("html/navbar.html", "r").read(), + "html_navbar": load_html("html/navbar.html"), + "book_descriptions": json.load(open("data/book_descriptions.json", "r")), + "chapter_summaries": json.load(open("data/chapter_summaries.json", "r")), + "html_chapter_nav": open("html/chapter_nav.html", "r").read(), + "pages": {}, "css_navbar": open("styles/header.css", "r").read(), "css_response": open("styles/human_readable.css", "r").read(), "css_collapsible": open("styles/collapsible.css", "r").read(), "css_page": open("styles/page.css", "r").read(), "js_collapsible": open("js/collapsible.js", "r").read(), "js_shortcode": open("js/shortcode.js", "r").read(), + "js_verse_menu": open("js/verse_menu.js", "r").read(), "favicon_svg": open("static/favicon.svg", "r").read(), "book_info": kjv_cur.execute( 'SELECT "order", title_short, category, otnt, chapters FROM book_info ORDER BY "order"' ).fetchall(), } +# Book name suggestions: the list of names is baked into the script once +cached["js_book_suggest"] = open("js/book_suggest.js", "r").read().replace( + "__BOOKS__", json.dumps(cached["book_names"]) +) + # Fill the chapter list with unique chapters for seeking back and forward by index. [ cached["chapter_list"].append(int(str(chap)[:-3])) @@ -166,14 +467,16 @@ for book in cached["book_info"]: book_order, title, category, testament, num_chapters = book testament_data[testament][category].append({ "title": title, - "chapters": num_chapters + "chapters": num_chapters, + "about": cached["book_descriptions"].get(title, ""), + "summaries": cached["chapter_summaries"].get(title, []), }) cached["book_info_for_index"] = dict(testament_data) # Render the index page once and cache it with app.app_context(): from flask import render_template_string - template_content = open("html/index.html", "r").read() + template_content = load_html("html/index.html") cached["html_index"] = render_template_string(template_content, testament_data=cached["book_info_for_index"]) @app.get("/js/shortcode.js") @@ -182,6 +485,18 @@ def serve_shortcode_js(): response.headers['Cache-Control'] = 'public, max-age=31536000' return response +@app.get("/js/book_suggest.js") +def serve_book_suggest_js(): + response = Response(cached.get("js_book_suggest"), mimetype='application/javascript') + response.headers['Cache-Control'] = 'public, max-age=31536000' + return response + +@app.get("/js/verse_menu.js") +def serve_verse_menu_js(): + response = Response(cached.get("js_verse_menu"), mimetype='application/javascript') + response.headers['Cache-Control'] = 'public, max-age=31536000' + return response + @app.get("/js/collapsible.js") def serve_collapsible_js(): response = Response(cached.get("js_collapsible"), mimetype='application/javascript') @@ -201,6 +516,49 @@ def serve_favicon(): response.headers['Cache-Control'] = 'public, max-age=31536000' return response +def static_page(path: str): + """Serve one of the simple html/ pages with its asset URLs versioned.""" + if path not in cached["pages"]: + cached["pages"][path] = load_html(path) + return Response(cached["pages"][path], mimetype="text/html") + + +def static_html_doc(title: str, body: str): + """Wrap a small html snippet in the standard page shell.""" + return ( + "" + "" + f"{title} - KJV API" + f"" + "
Home Quick Search
" + f"{body}" + ) + + +def chapter_find_link(name: str) -> str: + """Same link the verse headers use: the whole chapter, in the rich view.""" + return "/find?kw=" + urllib.parse.quote(name) + "&view=rich" + + +def render_navbar(query: str = "", chapter_nav: dict = None, bar_html: str = "") -> str: + """Opens the rich html document: , site header and the chapter bar. + + The caller adds the body content after it. bar_html replaces the chapter + bar for pages (like the reading plan) that bring their own. + """ + esc = lambda value: html_lib.escape(str(value), quote=True) + query = query or "" + title = esc(query.strip() or (chapter_nav or {}).get("current_chapter_name") or "Scripture") + chapter_html = bar_html + if chapter_nav: + values = {key: esc(value) for key, value in chapter_nav.items()} + values["current_chapter_link"] = esc(chapter_find_link(chapter_nav["current_chapter_name"])) + chapter_html = cached["html_chapter_nav"].format(**values) + return cached["html_navbar"].format( + title=title, css=cached["css_navbar"], query=esc(query), chapter_nav=chapter_html + ) + + def generate_short_id(): remain = random.randrange(614_656, 17_210_367) # resolves to length of 5 chars = "0123456789ACEFHJKLMNPRTUVWXY" @@ -511,27 +869,36 @@ def parse_query(query_override: str = None): return jsonify({"Error": "/find endpoint only supports rich responses"}) logging.info(f"Query: {query}") if not query: - return send_file("html/find.html") + return static_page("html/find.html") if query.endswith(","): query = query[:-1] - # Check if query contains both letters and digits (required for reference parsing) - # If only numbers or only letters, skip reference parsing and go to keyword search - has_letters = any(char.isalpha() for char in query) - has_digits = any(char.isdigit() for char in query) - - if not (has_letters and has_digits): - result = search() - else: + # A query that reads as scripture references ("Exodus", "1 jn 3.16") is shown + # as scripture; anything else is a keyword search. + resolved = resolve_references(query) + if resolved: result = show(kw=query, human_readable=human_readable) - if not hasattr(result, "__len__"): - logging.debug(f"parse_query.show = '{result}'") + if isinstance(result, str) and result != "Invalid Query": + notices = "" + for original, name in resolved["notes"]: + notices += ( + f"
Showing {html_lib.escape(name)} " + f"(interpreted from '{html_lib.escape(original)}')
" + ) + if resolved["bare"]: + keyword = urllib.parse.quote_plus(search_input_cleaning(query)) + notices += ( + f"
Showing {html_lib.escape(resolved['label'])}, " + f"chapter 1. Looking for the word instead? " + f"Search the text for '{html_lib.escape(query)}'
" + ) + if notices: + result = result.replace("
", "
" + notices, 1) return result - - if len(result) > 17: - return result - # Reference parsing failed, fall back to keyword search - result = search() + if not search_input_cleaning(query).strip(): + # nothing searchable left (punctuation, accents): show the search form + return static_page("html/find.html") + result = search() # Remove the closing
from search() so we can add dictionary content inside .responses if result.endswith("
"): result = result[:-6] # Remove last @@ -561,7 +928,7 @@ def parse_query(query_override: str = None): result += "" result += "

1828 Webster Definitions

Click on the word to get the definition.

" result += definitions_html - result += "" + result += f"" result += "" return result @@ -575,7 +942,7 @@ def define(word=None): if ref: ref = ref.strip() if not ref: - return send_file("html/dictionary.html") + return static_page("html/dictionary.html") dictionary = cached["json_dictionary"] ref = ref.lower() if not ref in dictionary.keys(): @@ -609,19 +976,249 @@ def return_dict_plain(ref, defs): def return_dict_rich(ref, defs, simple): if simple: css = "" + head = "" else: - css = cached["css_response"] + # standalone page: needs its own document head so phones scale it properly + css = "" + head = ( + "" + "" + "" + f"{html_lib.escape(ref.capitalize())} - KJV API" + + cached["css_navbar"] + + "
" + ) response = "" for e in defs: response += e + "

" return ( - cached["html_response"].format( - chapter=ref.split(":")[0], passage=ref.capitalize(), text=response + head + + cached["html_response"].format( + chapter=ref.split(":")[0], passage=ref.capitalize(), text=response, verse_id="" ) + css ) +# --------------------------------------------------------------------------- +# Scripture reference resolver. Understands the many ways people type a +# reference ("1 jn 3.16", "II Kings 2:1-4", "song of songs 2", "Exodus ") and +# turns it into ranges of verse ids (book*1000000 + chapter*1000 + verse). +# --------------------------------------------------------------------------- + +# Extra spellings people use that the abbreviations table does not know. +EXTRA_BOOK_ALIASES = { + "Song of Solomon": ["song of songs", "songs of solomon", "songs of songs", "song of sol", + "song of sol", "canticles", "canticle of canticles", "sos", "sng", "ss"], + "Psalms": ["psalm", "psalter", "pslm", "psa", "pss", "psm", "ps"], + "Revelation": ["revelations", "revelation of john", "revelation of st john", "the revelation", "apocalypse"], + "Ecclesiastes": ["ecclesiasts", "eccles", "ecc", "qoheleth"], + "Philemon": ["phlm", "phm", "philem"], + "Judges": ["jdg", "jdgs", "jg"], + "Mark": ["mrk", "mk", "mr"], + "John": ["jhn", "jn"], + "Acts": ["acts of the apostles", "act"], + "James": ["jas", "jm"], +} + +# Words that are books but also ordinary search words: a bare "numbers" or +# "mark" still shows the book, but /find offers a keyword search as well. +BARE_BOOK_EXTRA = { + "psalm", "songofsongs", "songsofsolomon", "songofsol", "canticles", "canticleofcanticles", + "revelations", "revelationofjohn", "therevelation", +} + +ROMAN_PREFIX = r"(?:iii|ii|i)(?=[\s.])" +ORDINAL_PREFIX = r"(?:1st|2nd|3rd|first|second|third)" +BOOK_PIECE_RE = re.compile( + r"^\s*(?P(?:(?:[1-3]|" + ROMAN_PREFIX + r"|" + ORDINAL_PREFIX + r")\.?\s*)?[a-z][a-z.\s]*?)" + r"\s*(?P\d[\d\s:.\-–—]*)?\s*$", + re.I, +) +CONTINUATION_RE = re.compile(r"^\s*\d[\d\s:.\-–—]*$") + + +def normalize_book_text(text: str) -> str: + """'I Cor.' / '1st Corinthians' / '1 corinthians' -> '1corinthians'""" + text = text.lower().strip() + text = re.sub(r"^(the\s+)?(book\s+of\s+)", "", text) + text = re.sub(r"^(1st|first)(?=[\s.a-z]|$)", "1", text) + text = re.sub(r"^(2nd|second)(?=[\s.a-z]|$)", "2", text) + text = re.sub(r"^(3rd|third)(?=[\s.a-z]|$)", "3", text) + text = re.sub(r"^iii(?=[\s.]|$)", "3", text) + text = re.sub(r"^ii(?=[\s.]|$)", "2", text) + text = re.sub(r"^i(?=[\s.])", "1", text) + return re.sub(r"[^a-z0-9]", "", text) + + +def build_book_aliases(): + """alias key -> book id, plus the canonical keys on their own""" + canonical = {} + aliases = {} + for book_id, name in cached["book_id_to_name"].items(): + canonical[normalize_book_text(name)] = book_id + aliases.update(canonical) + for abbreviation, book_id in kjv_cur.execute("SELECT a, b FROM key_abbreviations_english").fetchall(): + aliases.setdefault(normalize_book_text(abbreviation), book_id) + for name, extras in EXTRA_BOOK_ALIASES.items(): + book_id = lookup_book_id(name) + for extra in extras: + aliases[normalize_book_text(extra)] = book_id + return canonical, aliases + + +BOOK_CANONICAL, BOOK_ALIASES = build_book_aliases() +BOOK_ALIAS_KEYS = sorted(BOOK_ALIASES) +MAX_CHAPTER_ID_SPAN = 999 + + +def resolve_book(token: str, loose: bool = True): + """Returns (book_id, how) with how in exact|prefix|fuzzy, or None. + + loose=False only accepts full names and spellings that are unmistakably + a book (used for a bare word with no chapter number). + """ + key = normalize_book_text(token) + if not key: + return None + if not loose: + if key in BOOK_CANONICAL or key in BARE_BOOK_EXTRA: + return (BOOK_ALIASES[key], "exact") + # a clear misspelling of a long full name ("genisis") + if len(key) >= 6 and not key[0].isdigit(): + close = difflib.get_close_matches(key, list(BOOK_CANONICAL), n=1, cutoff=0.85) + if close: + return (BOOK_CANONICAL[close[0]], "fuzzy") + return None + if key in BOOK_ALIASES: + return (BOOK_ALIASES[key], "exact") + # unique (or best) prefix: "deut", "1cor", "matth" + prefixed = [k for k in BOOK_ALIAS_KEYS if k.startswith(key)] + if prefixed: + books = {BOOK_ALIASES[k] for k in prefixed} + canonical_hits = {BOOK_CANONICAL[k] for k in BOOK_CANONICAL if k.startswith(key)} + if len(canonical_hits) == 1: + return (next(iter(canonical_hits)), "prefix") + if len(books) == 1: + return (next(iter(books)), "prefix") + return (max(canonical_hits or books), "prefix") # ambiguous: the later book, as before + if len(key) >= 4: + close = difflib.get_close_matches(key, BOOK_ALIAS_KEYS, n=1, cutoff=0.78) + if close: + return (BOOK_ALIASES[close[0]], "fuzzy") + return None + + +def chapter_bounds(book_id: int, chapter: int) -> int: + """Clamp a chapter number into the book (0 -> 1, past the end -> last chapter).""" + return max(1, min(chapter, cached["books_chapter_lengths"][book_id])) + + +def parse_ref_rest(rest: str, book_id: int, previous=None): + """'3:16-18', '3 16', '3.16', '3-4', '3:16-4:2', '3', '3:-' -> (start_id, end_id|None, chapter, verse)""" + rest = rest.replace("–", "-").replace("—", "-") + rest = re.sub(r"\s*([:\-])\s*", r"\1", rest.strip()) + base = book_id * 1_000_000 + m = re.fullmatch(r"(\d+)", rest) + if m: + n = int(m.group(1)) + if previous and previous["verse"]: # "John 3:16, 18": a verse of the same chapter + chapter = previous["chapter"] + return (base + chapter * 1000 + n, None, chapter, n) + chapter = chapter_bounds(book_id, n) + return (base + chapter * 1000 + 1, base + chapter * 1000 + MAX_CHAPTER_ID_SPAN, chapter, 0) + m = re.fullmatch(r"(\d+)(?::-|-)", rest) # "3:-" or "3-": the whole chapter + if m: + chapter = chapter_bounds(book_id, int(m.group(1))) + return (base + chapter * 1000 + 1, base + chapter * 1000 + MAX_CHAPTER_ID_SPAN, chapter, 0) + m = re.fullmatch(r"(\d+)-(\d+)", rest) # chapters 3 to 4 + if m: + first, last = chapter_bounds(book_id, int(m.group(1))), chapter_bounds(book_id, int(m.group(2))) + if last < first: + first, last = last, first + return (base + first * 1000 + 1, base + last * 1000 + MAX_CHAPTER_ID_SPAN, first, 0) + m = re.fullmatch(r"(\d+)[:.\s](\d+)", rest) + if m: + chapter = chapter_bounds(book_id, int(m.group(1))) + verse = max(1, int(m.group(2))) + return (base + chapter * 1000 + verse, None, chapter, verse) + m = re.fullmatch(r"(\d+)[:.\s](\d+)-", rest) # "3:16-": from verse 16 to the end + if m: + chapter = chapter_bounds(book_id, int(m.group(1))) + return (base + chapter * 1000 + max(1, int(m.group(2))), base + chapter * 1000 + MAX_CHAPTER_ID_SPAN, chapter, int(m.group(2))) + m = re.fullmatch(r"(\d+)[:.\s](\d+)-(\d+)", rest) + if m: + chapter = chapter_bounds(book_id, int(m.group(1))) + first, last = sorted((max(1, int(m.group(2))), max(1, int(m.group(3))))) + return (base + chapter * 1000 + first, base + chapter * 1000 + last, chapter, first) + m = re.fullmatch(r"(\d+)[:.\s](\d+)-(\d+)[:.](\d+)", rest) # 3:16-4:2 across chapters + if m: + c1, c2 = chapter_bounds(book_id, int(m.group(1))), chapter_bounds(book_id, int(m.group(3))) + start = base + c1 * 1000 + max(1, int(m.group(2))) + end = base + c2 * 1000 + max(1, int(m.group(4))) + if end < start: + return None + return (start, end, c1, int(m.group(2))) + return None + + +def resolve_references(text: str, loose_bare: bool = False): + """Parse a whole query ('John 3:16, Romans 5:8') into verse id ranges. + + Returns None when it does not read as scripture references, so the + caller can treat it as a keyword search. Otherwise a dict with: + refs - list of (start_id, end_id or None) + notes - short explanations of guesses that were made + bare - the query was just a book name (shows its first chapter) + label - the name of the first book, for messages + """ + if not text or not text.strip(): + return None + pieces = [p for p in re.split(r"[,;\n]+", text) if p.strip()] + if not pieces or len(pieces) > 20: + return None + refs, notes = [], [] + bare = False + previous = None + label = None + for piece in pieces: + m = BOOK_PIECE_RE.match(piece) + if m: + token, rest = m.group("book"), m.group("rest") + if rest: + found = resolve_book(token) + else: + found = resolve_book(token, loose=loose_bare) + if not found: + return None + book_id, how = found + name = lookup_bookname(book_id) + if how == "fuzzy": + notes.append((token.strip(), name)) + if not rest: + if len(pieces) == 1: + bare = True + start, end, chapter, verse = (book_id * 1_000_000 + 1001, book_id * 1_000_000 + 1999, 1, 0) + else: + parsed = parse_ref_rest(rest, book_id, None) + if not parsed: + return None + start, end, chapter, verse = parsed + elif CONTINUATION_RE.match(piece) and previous: + book_id = previous["book"] + name = lookup_bookname(book_id) + parsed = parse_ref_rest(piece, book_id, previous) + if not parsed: + return None + start, end, chapter, verse = parsed + else: + return None + previous = {"book": book_id, "chapter": chapter, "verse": verse} + label = label or name + refs.append((start, end)) + return {"refs": refs, "notes": notes, "bare": bare, "label": label} + + @app.get("/show") # user facing to show a group of scripture. This will be deprecated and moved to backend. def show(kw=None, get_start_verse_id=False, human_readable=False): ref: str = request.args.get("kw", None) @@ -629,61 +1226,20 @@ def show(kw=None, get_start_verse_id=False, human_readable=False): if kw: ref = kw if not ref: - return send_file("html/show.html") + return static_page("html/show.html") logging.debug(f"show({ref})") - if ref.endswith(","): - ref = ref[:-1] - refs: list = [] - for r in ref.split(","): - r_ = ref_input_cleaning(r.strip()) - if r_: - refs.append(r_) - if not refs: + resolved = resolve_references(ref, loose_bare=True) + if not resolved: return "Invalid Query" + if get_start_verse_id: + return str(resolved["refs"][0][0]) verse_results: list = [] - # check if the search is for multiple refs - for ref in refs: - if len(ref) < 2: - continue - logging.debug(f"show.reference({ref})") - maybe_chapter: str = ref.split(":")[0][3:] - ref_chapter: str = "" - for maybedigit in maybe_chapter: - if maybedigit.isdigit(): - ref_chapter += maybedigit - ref_verse = ref.split(":")[1] - if ref.__contains__("-") or get_chapter: - if ref.endswith("-") or get_chapter: - start_verse = ref_verse.split("-")[0] - end_verse = "500" # just get all the verses if it ends with a dash - else: - start_verse, end_verse = ( - ref_verse.split("-")[0], - ref_verse.split("-")[1], - ) - else: - start_verse = ref_verse - end_verse = None - end_verse_id = None - book_str: str = ref_input_cleaning(ref, string.ascii_letters) - ref_bookname: str = ref_parse_book(book_str) - ref_book_id: int = lookup_book_id(ref_bookname) - if not ref_book_id: - return "Invalid Query" - maximum_chapters: int = cached.get("books_chapter_lengths").get(ref_book_id) - if int(ref_chapter) > maximum_chapters: - ref_chapter = str(maximum_chapters) - start_verse_id = get_verse_id(ref_book_id, ref_chapter, start_verse) - if get_start_verse_id: - return start_verse_id - if end_verse: - end_verse_id = get_verse_id(ref_book_id, ref_chapter, end_verse) - logging.debug(f"show.start_verse_id({start_verse_id})") - logging.debug(f"show.end_verse_id({end_verse_id})") - results = lookup_by_verse_id(start_verse_id, end_verse_id) + for start_id, end_id in resolved["refs"]: + if get_chapter: # ?all= : from the given verse to the end of its chapter + end_id = start_id // 1000 * 1000 + MAX_CHAPTER_ID_SPAN + results = lookup_by_verse_id(start_id, end_id) logging.debug(f"show.results len({len(results)})") - for result in results: - verse_results.append(result) + verse_results.extend(results) response_list = scripture_response(verse_results, human_readable) arg_view = request.args.get("view", None) if arg_view == "plain": @@ -722,7 +1278,7 @@ def response_rich(scripture): text = str(scripture[4]) passage = bookname + " " + chapter + ":" + verse return cached["html_response"].format( - chapter=passage.split(":")[0], passage=passage, text=text + chapter=passage.split(":")[0], passage=passage, text=text, verse_id=scripture[0] ) @@ -739,22 +1295,18 @@ def scripture_response(scriptures, human_readable=False): except IndexError: logging.error(f"Malformed query: {request.args.get('kw',None)}") return(("Malformed Query - if you believe this to be in error (your query should resolve), email tyler@dinsmoor.us",)) - l_r_nav = get_next_prev_chapter( - str(scriptures[0][0]) - ) # get the first scripture, the id thereof + # Prev/next chapter links only make sense when reading a single chapter, + # not for a keyword search that spans books. + l_r_nav = None + if len({str(scripture[0])[:-3] for scripture in scriptures}) == 1: + l_r_nav = get_next_prev_chapter( + str(scriptures[0][0]) + ) # get the first scripture, the id thereof # response.append(cached["css_response"]) response.append( - cached["html_navbar"].format( - query=request.args.get("kw", ""), - current_chapter_name=l_r_nav["current_chapter_name"], - next_chapter_name=l_r_nav["next_chapter_name"], - next_chapter_link=l_r_nav["next_chapter_link"], - prev_chapter_name=l_r_nav["prev_chapter_name"], - prev_chapter_link=l_r_nav["prev_chapter_link"], - ) + render_navbar(request.args.get("kw", ""), l_r_nav) ) - response.append(cached["css_navbar"]) response.append("
") response.append(f"
{len(scriptures)} Results
") for scripture in scriptures: @@ -787,11 +1339,11 @@ def idx(shortcode=""): @app.get("/fe") def frontend(): - return send_file('kjv_fe.html') + return static_page("kjv_fe.html") @app.get("/salvation") # a web page about going to heaven def salvation(): - return send_file("html/heaven.html") + return static_page("html/heaven.html") @app.get("/t/random", defaults={"view": "plain"}) # backwards-compatibility plaintext random Bible verse @@ -851,9 +1403,9 @@ def search(): if keywords: cleaned_keywords = search_input_cleaning(keywords) if not cleaned_keywords: - return send_file("html/search.html") + return static_page("html/search.html") else: - return send_file("html/search.html") + return static_page("html/search.html") # Check if this is a common theological concept not in KJV concept_mapping = cached["concept_mappings"].get(cleaned_keywords.lower(), None) @@ -1054,15 +1606,7 @@ def search(): if arg_view == "rich": # Build a nice no-results page with header - no_results_html = cached["html_navbar"].format( - query=cleaned_keywords, - current_chapter_name="", - next_chapter_name="", - next_chapter_link="", - prev_chapter_name="", - prev_chapter_link="" - ) - no_results_html += cached["css_navbar"] + no_results_html = render_navbar(cleaned_keywords) no_results_html += "
" no_results_html += f"
No results found for '{cleaned_keywords}'.
" @@ -1143,7 +1687,7 @@ def search(): @app.get("/browse") # not implemented. TO be moved to frontend. def bible_reader(): - return render_template("reader.html", books=cached.get("book_names")) + return render_template("reader.html", books=cached.get("book_names"), v=ASSET_VERSION) # We should return an index of all books with # drop down menus of each chapter, and a search # bar at the top for quick seeking, which can use @@ -1153,23 +1697,17 @@ def bible_reader(): # Maybe preliminary theming support -@app.get("/plan") # not implmeneted. To be moved to frontend. -def reading_plan(): - # needs 2 sets of data, books and time frame. - # should default to the entire Bible in 1 year. - # should start today() and end today()+1year - # Should return reading_plan template. - return render_template("reading_plan.html") - - @app.get("/status") # basic instance info def status(): import resource - return f"""Sanity: {sanity}
-UserMode: {usermode}
-Memory: {resource.getrusage(resource.RUSAGE_SELF).ru_maxrss} - """ + return static_html_doc( + "Status", + f"""

Status

+

Sanity: {sanity}

+

UserMode: {usermode}

+

Memory: {resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}

""", + ) @app.get("/seqreport") # shows current tracked scripture sequences @@ -1180,7 +1718,7 @@ def sequential_report(uid=None): seq_conn = sqlite3.connect("data/seq.db") seq_cur = seq_conn.cursor() uids = seq_cur.execute("select * from reading_tracker").fetchall() - return render_template("seq_report.html", uids=uids) + return render_template("seq_report.html", uids=uids, v=ASSET_VERSION) @app.get("/screport") # shows current remembered shortcodes def shortcode_report(shortcode=None): @@ -1192,7 +1730,7 @@ def shortcode_report(shortcode=None): shortcodes = short_cur.execute( "SELECT * FROM shortcodes" ).fetchall() - return render_template("shortcode_report.html", shortcodes=shortcodes) + return render_template("shortcode_report.html", shortcodes=shortcodes, v=ASSET_VERSION) def sequential_purge(): @@ -1226,7 +1764,7 @@ def sequential_read(start=0, num=1, uid=None, getuid=False): except ValueError: return "Invalid num argument" if not uid and not getuid: - return send_file("html/seq.html") + return static_page("html/seq.html") if usermode == "Single": seq_conn = sqlite3.connect("data/seq.db", check_same_thread=False) else: diff --git a/data/book_descriptions.json b/data/book_descriptions.json new file mode 100644 index 0000000..bf2d952 --- /dev/null +++ b/data/book_descriptions.json @@ -0,0 +1,68 @@ +{ + "Genesis": "Creation, the fall, the flood, and the patriarchs Abraham, Isaac, Jacob and Joseph.", + "Exodus": "Israel's deliverance from Egypt, the Law at Sinai, and the building of the tabernacle.", + "Leviticus": "God's laws for offerings, priests, holiness and the feasts of Israel.", + "Numbers": "Israel's census and forty years of wandering in the wilderness.", + "Deuteronomy": "Moses' final sermons, restating the Law before Israel enters the promised land.", + "Joshua": "Israel crosses the Jordan, conquers Canaan and divides the land.", + "Judges": "A cycle of sin and rescue as God raises up judges over Israel.", + "Ruth": "A Moabite widow's faithfulness and her redeemer Boaz, an ancestor of David.", + "1 Samuel": "Samuel, the rise and failure of King Saul, and David's anointing.", + "2 Samuel": "David's reign over Israel, his sin with Bathsheba, and the troubles that followed.", + "1 Kings": "Solomon's glory and temple, the divided kingdom, and the prophet Elijah.", + "2 Kings": "The decline of Israel and Judah to exile in Assyria and Babylon.", + "1 Chronicles": "Genealogies from Adam and the reign of David, with a focus on worship.", + "2 Chronicles": "Solomon's temple and the kings of Judah, ending with the exile.", + "Ezra": "The return from Babylon and the rebuilding of the temple.", + "Nehemiah": "The rebuilding of Jerusalem's walls and the people's renewed covenant.", + "Esther": "A Jewish queen in Persia saves her people from Haman's plot.", + "Job": "A righteous man suffers loss and questions, and God answers him.", + "Psalms": "Songs and prayers of praise, lament, trust and thanksgiving.", + "Proverbs": "Short sayings of wisdom for godly living.", + "Ecclesiastes": "The Preacher finds life under the sun empty without fearing God.", + "Song of Solomon": "A poem of love between a bridegroom and his bride.", + "Isaiah": "Prophecies of judgment and comfort, and of the coming Messiah and His kingdom.", + "Jeremiah": "The weeping prophet warns Judah of Babylon's coming and promises a new covenant.", + "Lamentations": "Poems of grief over the fall of Jerusalem.", + "Ezekiel": "Visions of judgment on Jerusalem, and of Israel's restoration and a new temple.", + "Daniel": "Faithful Daniel in Babylon, the lions' den, and visions of coming kingdoms.", + "Hosea": "God's faithful love for unfaithful Israel, pictured in the prophet's marriage.", + "Joel": "A plague of locusts calls Judah to repent and promises the Spirit poured out.", + "Amos": "A herdsman prophet condemns injustice and warns Israel of judgment.", + "Obadiah": "Judgment on Edom for its pride and its violence against Judah.", + "Jonah": "A reluctant prophet flees God, is swallowed by a fish, and preaches to Nineveh.", + "Micah": "Judgment on Israel's rulers, and the promise of a ruler born in Bethlehem.", + "Nahum": "The coming destruction of Nineveh.", + "Habakkuk": "The prophet questions God about evil and learns that the just shall live by faith.", + "Zephaniah": "The coming day of the LORD and a promise to restore a remnant.", + "Haggai": "A call to the returned exiles to rebuild the temple.", + "Zechariah": "Visions and prophecies encouraging the rebuilding, and of the coming King.", + "Malachi": "A last warning to a careless people, and the promise of a messenger before the LORD.", + "Matthew": "The gospel of Jesus Christ as the promised King of the Jews.", + "Mark": "A fast-moving gospel of Jesus as the servant who died and rose again.", + "Luke": "A careful account of the life of Jesus, the Son of man who came to seek the lost.", + "John": "The gospel of Jesus as the Son of God, written that you might believe and have life.", + "Acts": "The apostles spread the gospel from Jerusalem to Rome after Christ's ascension.", + "Romans": "Salvation by grace through faith, for Jew and Gentile alike.", + "1 Corinthians": "Paul corrects divisions and disorder in the church, and teaches on love and the resurrection.", + "2 Corinthians": "Paul defends his ministry and encourages comfort and generosity.", + "Galatians": "Freedom in Christ: we are justified by faith, not by the works of the law.", + "Ephesians": "The church as Christ's body, saved by grace and called to walk worthy.", + "Philippians": "A joyful letter on Christ's humility and rejoicing in all circumstances.", + "Colossians": "Christ is supreme over all, and complete in Him.", + "1 Thessalonians": "Encouragement to the young church, and the hope of Christ's return.", + "2 Thessalonians": "Correction about the day of the Lord, and a call to steadfast work.", + "1 Timothy": "Paul's instructions to a young pastor on church order and sound doctrine.", + "2 Timothy": "Paul's last letter, urging Timothy to be faithful and preach the word.", + "Titus": "Qualifications for elders and good works in the churches of Crete.", + "Philemon": "Paul appeals to Philemon to receive back his runaway servant Onesimus as a brother.", + "Hebrews": "Christ is better than the angels, Moses and the priests, and faith endures.", + "James": "Practical faith: be doers of the word, control the tongue, and pray.", + "1 Peter": "Hope and holy living for believers who suffer for Christ.", + "2 Peter": "Grow in grace and knowledge, and beware of false teachers and scoffers.", + "1 John": "Fellowship with God: walk in light, love one another, and know you have eternal life.", + "2 John": "A short warning to walk in truth and love and to avoid deceivers.", + "3 John": "Praise for hospitality to travelling believers, and a rebuke of Diotrephes.", + "Jude": "A call to contend for the faith against ungodly men.", + "Revelation": "Visions of Christ, the churches, coming judgments, and the new heaven and earth." +} \ No newline at end of file diff --git a/data/chapter_summaries.json b/data/chapter_summaries.json new file mode 100644 index 0000000..fd68742 --- /dev/null +++ b/data/chapter_summaries.json @@ -0,0 +1,1323 @@ +{ +"Genesis": [ +"Creation of the heavens and earth", +"The Garden of Eden; Adam and Eve", +"The serpent and the fall", +"Cain and Abel", +"Adam's line to Noah", +"Wickedness grows; Noah told to build the ark", +"The flood begins", +"The flood ends; Noah leaves the ark", +"God's covenant and the rainbow", +"The nations descended from Noah", +"The tower of Babel", +"God calls Abram", +"Abram and Lot part ways", +"Abram rescues Lot; Melchizedek", +"God's covenant with Abram", +"Hagar and Ishmael", +"The covenant of circumcision", +"Three visitors; Abraham pleads for Sodom", +"Sodom destroyed", +"Abraham and Abimelech", +"Isaac is born", +"Abraham offers Isaac", +"Sarah dies and is buried", +"A wife for Isaac: Rebekah", +"Abraham dies; Esau and Jacob born", +"Isaac, Abimelech and the wells", +"Jacob receives Isaac's blessing", +"Jacob's dream at Bethel", +"Jacob marries Leah and Rachel", +"Jacob's sons born; his flocks increase", +"Jacob flees Laban", +"Jacob wrestles with God", +"Jacob and Esau reconcile", +"Dinah and Shechem", +"Jacob returns to Bethel; Rachel dies", +"Esau's descendants", +"Joseph's dreams; sold by his brothers", +"Judah and Tamar", +"Joseph in Potiphar's house", +"Joseph interprets the prisoners' dreams", +"Pharaoh's dreams; Joseph made ruler", +"Joseph's brothers come to Egypt", +"The brothers return with Benjamin", +"The silver cup in Benjamin's sack", +"Joseph reveals himself", +"Jacob goes down to Egypt", +"Israel settles in Goshen", +"Jacob blesses Ephraim and Manasseh", +"Jacob blesses his twelve sons", +"Jacob and Joseph die" +], +"Exodus": [ +"Israel oppressed; Moses is born", +"Moses flees to Midian", +"The burning bush", +"Moses' signs; Aaron joins him", +"Pharaoh increases the work", +"God promises deliverance", +"Aaron's rod; water turned to blood", +"Plagues of frogs and lice", +"Plagues of flies, livestock and boils", +"Hail and locusts", +"Plague of darkness", +"The Passover instituted; the firstborn die", +"Consecration of the firstborn; the pillar of cloud and fire", +"Crossing the Red Sea", +"The song of Moses; bitter waters", +"Manna and quail", +"Water from the rock; Amalek defeated", +"Jethro advises Moses", +"Israel at Mount Sinai", +"The Ten Commandments", +"Laws on servants and injuries", +"Laws on property and neighbors", +"Laws of justice, the feasts and the angel", +"The covenant confirmed; Moses on the mount", +"Offerings; the ark, table and lampstand", +"The tabernacle curtains", +"The altar and the court", +"The priests' garments", +"Consecration of the priests", +"The incense altar and atonement money", +"Bezaleel and the sabbath", +"The golden calf", +"Moses pleads; sees God's glory", +"The second tablets; Moses' shining face", +"The sabbath and the offerings", +"The craftsmen build the tabernacle", +"The ark, table and lampstand made", +"The altar and court made", +"The priests' garments made", +"The tabernacle raised; glory fills it" +], +"Leviticus": [ +"The burnt offering", +"The grain offering", +"The peace offering", +"The sin offering", +"Sins needing a trespass offering", +"Laws of the trespass offering", +"Priests' portions and the peace offering", +"Aaron and his sons consecrated", +"The priests begin their ministry", +"Nadab and Abihu die", +"Clean and unclean animals", +"Purification after childbirth", +"Laws on leprosy", +"Cleansing of leprosy and houses", +"Bodily uncleanness", +"The Day of Atonement", +"Blood and the place of sacrifice", +"Unlawful relations forbidden", +"Laws of holiness and neighbor love", +"Warnings against Molech and the occult", +"Holiness of priests", +"Priests' offerings", +"The feasts of the LORD", +"The lamps, showbread and a blasphemer", +"The sabbatical and jubilee years", +"Blessings and curses for obedience", +"Laws on vows and tithes" +], +"Numbers": [ +"The first census", +"The camp and tribes arranged", +"The Levites numbered", +"Duties of the Levites", +"Unclean persons; the jealousy offering", +"The Nazarite vow; the priestly blessing", +"Offerings of the tribal leaders", +"The lamps; Levites cleansed", +"Passover and the cloud", +"The silver trumpets; Israel departs Sinai", +"The people complain; quail sent", +"Miriam and Aaron oppose Moses", +"Twelve spies sent into Canaan", +"Israel rebels; forty years decreed", +"Laws of offerings; the sabbath-breaker", +"Korah's rebellion", +"Aaron's rod buds", +"Duties and dues of priests and Levites", +"The red heifer", +"Water from the rock; Aaron dies", +"The bronze serpent; victories over Sihon and Og", +"Balak summons Balaam; Balaam's donkey", +"Balaam's first oracles", +"Balaam's further blessings", +"Israel's sin at Peor", +"The second census", +"Zelophehad's daughters; Joshua named", +"Daily and sabbath offerings", +"The festival offerings", +"Laws on vows", +"Vengeance on Midian", +"Reuben and Gad settle east of Jordan", +"Israel's journey recalled", +"Boundaries of Canaan", +"Cities of the Levites and cities of refuge", +"Zelophehad's daughters' inheritance" +], +"Deuteronomy": [ +"Moses recalls leaving Horeb", +"The wilderness wanderings", +"Victory over Og of Bashan", +"Obey the statutes; idolatry forbidden", +"The Ten Commandments repeated", +"Love the LORD with all your heart", +"Drive out the nations", +"Remember the wilderness; man lives by God's word", +"Israel's stubbornness at Horeb", +"The second tablets; what God requires", +"Love and obey for blessing", +"One place of worship", +"Beware of false prophets", +"Clean foods and tithes", +"The sabbatical year and freeing servants", +"Passover, Weeks and Tabernacles", +"Judges, kings and idolatry", +"Priests' portions; true and false prophets", +"Cities of refuge and witnesses", +"Laws of warfare", +"Unsolved murder; family laws", +"Assorted laws of kindness and purity", +"The congregation; vows and kindness", +"Marriage, divorce and fair dealing", +"Justice, and a brother's duty", +"Firstfruits and tithes", +"Blessings and curses on Ebal and Gerizim", +"Blessings of obedience, curses of disobedience", +"The covenant in Moab", +"Return and the choice of life", +"Moses' final words; Joshua succeeds", +"The song of Moses", +"Moses blesses the tribes", +"Moses views the land and dies" +], +"Joshua": [ +"Joshua commissioned", +"Rahab hides the spies", +"Crossing the Jordan", +"The memorial stones", +"Circumcision at Gilgal", +"The fall of Jericho", +"Achan's sin", +"Ai conquered; the covenant at Ebal", +"The Gibeonites' deception", +"The sun stands still", +"Defeat of the northern kings", +"List of conquered kings", +"Land yet to be possessed", +"Caleb receives Hebron", +"Judah's allotment", +"Ephraim's allotment", +"Manasseh's allotment", +"The tabernacle at Shiloh; Benjamin's land", +"Allotments for the remaining tribes", +"Cities of refuge", +"Cities of the Levites", +"The eastern tribes return home", +"Joshua's farewell", +"The covenant at Shechem" +], +"Judges": [ +"Judah and Simeon fight the Canaanites", +"The angel at Bochim; the cycle of sin begins", +"Othniel, Ehud and Shamgar", +"Deborah and Barak", +"The song of Deborah", +"Gideon is called", +"Gideon's fleece and the 300", +"Gideon defeats Midian; the ephod", +"Abimelech's wicked reign", +"Tola, Jair and the Ammonite threat", +"Jephthah's vow", +"The Shibboleth; judges Ibzan, Elon, Abdon", +"Samson's birth foretold", +"Samson's marriage and riddle", +"Samson and the Philistines", +"Samson and Delilah", +"Micah's idol and the Levite", +"The Danites take Laish", +"The Levite's concubine at Gibeah", +"Israel's war against Benjamin", +"Wives found for the Benjamites" +], +"Ruth": [ +"Naomi and Ruth return to Bethlehem", +"Ruth gleans in Boaz's field", +"Ruth at the threshing floor", +"Boaz redeems Ruth; the line of David" +], +"1 Samuel": [ +"Hannah's prayer; Samuel born", +"Hannah's song; Eli's sons", +"The LORD calls Samuel", +"Israel loses the ark", +"The ark in Philistia", +"The ark returns", +"Samuel judges at Mizpeh", +"Israel demands a king", +"Saul meets Samuel", +"Saul anointed king", +"Saul delivers Jabesh-gilead", +"Samuel's farewell", +"Saul's unlawful offering", +"Jonathan's victory at Michmash", +"Saul rejected for disobeying", +"David anointed", +"David and Goliath", +"Saul's jealousy; Jonathan's love", +"David escapes Saul's attempts", +"David and Jonathan's covenant", +"David at Nob and Gath", +"David in the cave of Adullam", +"David saves Keilah", +"David spares Saul in the cave", +"Samuel dies; Nabal and Abigail", +"David spares Saul again", +"David flees to Gath", +"Saul and the witch of Endor", +"David dismissed by the Philistines", +"David recovers from Ziklag", +"Saul and his sons die" +], +"2 Samuel": [ +"David mourns Saul and Jonathan", +"David made king of Judah", +"Abner joins David and is murdered", +"Ish-bosheth murdered", +"David king over all Israel", +"The ark brought up; Uzzah dies", +"God's covenant with David", +"David's victories", +"David honors Mephibosheth", +"The Ammonite war begins", +"David and Bathsheba", +"Nathan rebukes David; the child dies", +"Amnon and Tamar", +"Absalom returns from exile", +"Absalom's conspiracy", +"David flees Jerusalem", +"Hushai counters Ahithophel", +"Absalom killed", +"David returns to Jerusalem", +"Sheba's rebellion", +"The Gibeonites avenged", +"David's song of deliverance", +"David's mighty men", +"David's census and the plague" +], +"1 Kings": [ +"Adonijah's plot; Solomon crowned", +"David's charge; Solomon secures the throne", +"Solomon asks for wisdom; the two mothers", +"Solomon's officers and wisdom", +"Preparations to build the temple", +"Solomon builds the temple", +"The temple furnishings; Solomon's house", +"The temple dedicated", +"God appears to Solomon again", +"The queen of Sheba visits", +"Solomon's wives; adversaries rise", +"The kingdom divided; Jeroboam's golden calves", +"The man of God from Judah", +"Jeroboam and Rehoboam die", +"Kings Abijam and Asa", +"Kings of Israel; Ahab begins to reign", +"Elijah and the widow's son", +"Elijah on Mount Carmel", +"Elijah at Horeb", +"Ahab defeats Syria", +"Naboth's vineyard", +"Ahab and Micaiah; Ahab dies" +], +"2 Kings": [ +"Elijah and Ahaziah", +"Elijah taken up; Elisha succeeds", +"Moab's rebellion", +"The widow's oil; the Shunammite's son", +"Naaman healed of leprosy", +"The floating axe head", +"Syria besieges Samaria", +"The Shunammite's land restored", +"Jehu anointed king", +"Jehu destroys Ahab's house", +"Joash made king of Judah", +"Joash repairs the temple", +"Jehoahaz and Jehoash of Israel", +"Amaziah and Jeroboam II", +"Azariah and other kings", +"Ahaz of Judah", +"Israel falls to Assyria", +"Hezekiah's reforms", +"Sennacherib threatens Jerusalem", +"Hezekiah's illness and the sun dial", +"Manasseh and Amon", +"Josiah finds the book of the law", +"Josiah's reforms", +"Josiah dies; Judah's last kings", +"Jerusalem destroyed; Judah exiled" +], +"1 Chronicles": [ +"Genealogy from Adam to Abraham", +"The sons of Israel and Judah", +"David's family and Solomon's line", +"Judah's descendants", +"Reuben, Gad and half Manasseh", +"The sons of Levi", +"Issachar, Benjamin and others", +"Benjamin's line", +"Returned exiles; Saul's family", +"Saul's death", +"David made king; his mighty men", +"Men who joined David", +"David brings the ark; Uzzah", +"David's palace and victories", +"The ark brought to Jerusalem", +"The ark in the tent; David's psalm", +"God's covenant with David", +"David's victories", +"David defeats Ammon and Syria", +"Rabbah captured", +"David's sin in numbering Israel", +"Preparations for the temple", +"The Levites organized", +"Priestly and Levitical divisions", +"The singers", +"Gatekeepers and treasurers", +"David's military and civil officers", +"David's charge to Solomon", +"David's prayer; Solomon crowned" +], +"2 Chronicles": [ +"Solomon's wisdom and wealth", +"Solomon prepares to build the temple", +"The temple's construction", +"The temple furnishings", +"The ark brought into the temple", +"Solomon's dedication prayer", +"The glory fills the temple; God appears", +"Solomon's accomplishments", +"The queen of Sheba visits", +"The kingdom divided", +"Rehoboam's reign", +"Shishak invades Judah", +"Abijah's victory over Jeroboam", +"Asa's reign and victory", +"Asa's reforms", +"Asa's later years", +"Jehoshaphat's reign", +"Jehoshaphat and Ahab", +"Jehoshaphat appoints judges", +"Jehoshaphat's victory by praise", +"Jehoram's wicked reign", +"Ahaziah and Athaliah", +"Joash crowned", +"Joash repairs the temple", +"Amaziah's reign", +"Uzziah's pride and leprosy", +"Jotham's reign", +"Ahaz's idolatry", +"Hezekiah cleanses the temple", +"Hezekiah's great Passover", +"Hezekiah's reforms", +"Sennacherib's invasion", +"Manasseh and Amon", +"Josiah finds the book of the law", +"Josiah's Passover and death", +"Judah's last kings; Cyrus' decree" +], +"Ezra": [ +"Cyrus allows the Jews to return", +"The list of returning exiles", +"The altar and temple foundation", +"Opposition to the building", +"Rebuilding resumes under Haggai", +"Darius confirms the decree", +"Temple dedicated; Passover kept", +"Ezra comes to Jerusalem", +"Ezra and the people confess sin", +"Foreign wives put away" +], +"Nehemiah": [ +"Nehemiah's prayer for Jerusalem", +"Nehemiah goes to rebuild", +"The builders of the wall", +"Sanballat's opposition", +"Nehemiah ends oppression", +"The wall finished", +"Registry of the returnees", +"Ezra reads the law", +"Israel confesses and prays", +"The covenant sealed", +"The people who settled Jerusalem", +"The wall dedicated", +"Nehemiah's final reforms" +], +"Esther": [ +"Queen Vashti deposed", +"Esther becomes queen", +"Haman plots against the Jews", +"Mordecai urges Esther to act", +"Esther's banquet", +"Haman honors Mordecai", +"Haman hanged", +"The Jews permitted to defend themselves", +"The feast of Purim", +"Mordecai's greatness" +], +"Job": [ +"Satan tests Job", +"Job's second test", +"Job curses the day of his birth", +"Eliphaz speaks", +"Eliphaz continues", +"Job answers Eliphaz", +"Job pleads with God", +"Bildad speaks", +"Job answers Bildad", +"Job's complaint", +"Zophar speaks", +"Job defends himself", +"Job appeals to God", +"Man's days are few", +"Eliphaz speaks again", +"Job laments", +"Job's hope is in the grave", +"Bildad speaks again", +"I know that my redeemer liveth", +"Zophar speaks again", +"Job: the wicked prosper", +"Eliphaz accuses Job", +"Job longs to find God", +"Why are times not hidden from the Almighty", +"Bildad's short speech", +"Job's reply", +"Job maintains his integrity", +"Wisdom's hidden worth", +"Job's former blessing", +"Job's present misery", +"Job's final defense", +"Elihu begins", +"Elihu: God speaks to men", +"Elihu defends God's justice", +"Elihu rebukes Job", +"Elihu on God's greatness", +"God's majesty in the storm", +"The LORD answers from the whirlwind", +"The LORD's creatures", +"Job's humility; Behemoth", +"Leviathan", +"Job repents and is restored" +], +"Psalms": [ +"The blessed man and the ungodly", +"Why do the heathen rage", +"Lord, how many are my foes", +"Evening prayer for peace", +"Morning prayer for guidance", +"Rebuke me not in Thine anger", +"David pleads his innocence", +"How excellent is Thy name", +"I will praise the LORD for His judgments", +"Why standest Thou afar off", +"In the LORD put I my trust", +"Help, LORD; the faithful fail", +"How long, O LORD", +"The fool says there is no God", +"Who shall dwell in Thy tabernacle", +"Thou wilt not leave my soul in hell", +"Hear my righteous cause", +"The LORD is my rock and deliverer", +"The heavens declare the glory of God", +"The LORD hear thee in the day of trouble", +"The king rejoices in God's salvation", +"My God, why hast Thou forsaken me", +"The LORD is my shepherd", +"The earth is the LORD's", +"Unto Thee, O LORD, do I lift up my soul", +"I have walked in my integrity", +"The LORD is my light and salvation", +"Unto Thee will I cry", +"Give unto the LORD glory and strength", +"Thou hast lifted me up", +"In Thee, O LORD, do I put my trust", +"Blessed is he whose transgression is forgiven", +"Rejoice in the LORD, ye righteous", +"I will bless the LORD at all times", +"Plead my cause, O LORD", +"The transgression of the wicked", +"Fret not because of evildoers", +"Rebuke me not in Thy wrath", +"I said, I will take heed to my ways", +"I waited patiently for the LORD", +"Blessed is he that considers the poor", +"As the hart pants after the water brooks", +"Send out Thy light and Thy truth", +"We have heard with our ears, O God", +"A song of the royal wedding", +"God is our refuge and strength", +"Clap your hands, all ye people", +"Great is the LORD in the city of our God", +"Hear this, all ye people", +"The mighty God has spoken", +"Have mercy upon me, O God", +"Why boast of evil, O mighty man", +"The fool has said there is no God", +"Save me, O God, by Thy name", +"Cast thy burden upon the LORD", +"Be merciful unto me; I trust in Thee", +"Be merciful; my soul trusts in Thee", +"Do ye indeed speak righteousness", +"Deliver me from mine enemies", +"O God, Thou hast cast us off", +"Hear my cry, O God", +"My soul waits upon God", +"O God, Thou art my God; early will I seek Thee", +"Hide me from the secret counsel of the wicked", +"Praise waits for Thee in Zion", +"Make a joyful noise unto God", +"God be merciful unto us and bless us", +"Let God arise", +"Save me, O God; the waters have come in", +"Make haste, O God, to deliver me", +"In Thee, O LORD, do I put my trust", +"A prayer for the king and his righteous reign", +"Truly God is good to Israel", +"O God, why hast Thou cast us off", +"We give thanks unto Thee, O God", +"In Judah is God known", +"I cried unto God with my voice", +"Give ear, O my people", +"O God, the heathen are come into Thine inheritance", +"Hear, O Shepherd of Israel", +"Sing aloud unto God our strength", +"God stands in the congregation of the mighty", +"Keep not silence, O God", +"How amiable are Thy tabernacles", +"LORD, Thou hast been favorable unto Thy land", +"Bow down Thine ear, O LORD", +"Glorious things are spoken of Zion", +"O LORD, my cry before Thee", +"I will sing of the mercies of the LORD", +"Lord, Thou hast been our dwelling place", +"He that dwells in the secret place of the Most High", +"It is a good thing to give thanks", +"The LORD reigns; He is clothed with majesty", +"O LORD God, to whom vengeance belongs", +"O come, let us sing unto the LORD", +"O sing unto the LORD a new song", +"The LORD reigns; let the earth rejoice", +"Sing unto the LORD; He has done marvelous things", +"The LORD reigns; let the people tremble", +"Make a joyful noise unto the LORD", +"I will sing of mercy and judgment", +"Hear my prayer; my days consume like smoke", +"Bless the LORD, O my soul", +"Creation's praise", +"Remember His wonders for Israel", +"The mercies of the LORD despite Israel's rebellion", +"Let the redeemed of the LORD say so", +"O God, my heart is fixed", +"Hold not Thy peace, O God of my praise", +"The LORD said unto my Lord, sit at My right hand", +"The works of the LORD are great", +"Blessed is the man that fears the LORD", +"Praise, O ye servants of the LORD", +"When Israel went out of Egypt", +"Not unto us, O LORD, but unto Thy name", +"I love the LORD, because He has heard my voice", +"O praise the LORD, all ye nations", +"O give thanks unto the LORD, for He is good", +"Blessed are the undefiled in the way", +"In my distress I cried unto the LORD", +"I will lift up mine eyes unto the hills", +"I was glad when they said, Let us go to the house of the LORD", +"Unto Thee lift I up mine eyes", +"If it had not been the LORD who was on our side", +"They that trust in the LORD are as Mount Zion", +"When the LORD turned again the captivity of Zion", +"Except the LORD build the house", +"Blessed is every one that fears the LORD", +"Many a time have they afflicted me", +"Out of the depths have I cried", +"LORD, my heart is not haughty", +"LORD, remember David", +"How good it is for brethren to dwell together in unity", +"Behold, bless ye the LORD", +"Praise ye the name of the LORD", +"For His mercy endures forever", +"By the rivers of Babylon", +"I will praise Thee with my whole heart", +"O LORD, Thou hast searched me and known me", +"Deliver me, O LORD, from the evil man", +"LORD, I cry unto Thee; make haste", +"I cried unto the LORD with my voice", +"Hear my prayer, O LORD", +"Blessed be the LORD my strength", +"I will extol Thee, my God, O king", +"Praise the LORD, O my soul", +"Praise ye the LORD; it is good to sing", +"Praise ye the LORD from the heavens", +"Sing unto the LORD a new song", +"Praise God in His sanctuary" +], +"Proverbs": [ +"The beginning of knowledge; wisdom calls", +"Wisdom keeps from evil", +"Trust in the LORD with all thine heart", +"A father's counsel to get wisdom", +"Beware of the immoral woman", +"Warnings against surety and laziness", +"Warnings against adultery", +"Wisdom's call and her origin", +"Wisdom's feast and the fool's", +"The righteous and the wicked contrasted", +"Integrity, honesty and kindness", +"The upright and the foolish", +"Discipline and the wise", +"The way of the wise", +"A soft answer turns away wrath", +"Pride goes before destruction", +"A friend loves at all times", +"The power of the tongue", +"Strife, laziness and wise counsel", +"Wine is a mocker", +"The king's heart is in the LORD's hand", +"A good name is better than riches", +"Do not envy sinners; avoid drunkenness", +"Do not envy evil men", +"Proverbs copied by Hezekiah's men", +"Fools and sluggards", +"Do not boast of tomorrow", +"The wicked flee when none pursues", +"Rod and reproof; the poor and rulers", +"The words of Agur", +"The words of Lemuel; the virtuous woman" +], +"Ecclesiastes": [ +"Vanity of vanities", +"The vanity of pleasure and labor", +"A time for everything", +"Oppression and the value of companions", +"Fear God; riches are vain", +"Riches cannot satisfy", +"A good name and wisdom", +"The wicked and the just", +"Death comes to all", +"Wisdom and folly", +"Cast thy bread upon the waters", +"Remember thy Creator in youth" +], +"Song of Solomon": [ +"The bride longs for her beloved", +"The beloved among the lilies", +"The bride seeks her beloved", +"The beloved's beauty", +"The bride's search and praise", +"The bride's love and beauty", +"The beloved's delight", +"Set me as a seal upon thy heart" +], +"Isaiah": [ +"Judah's rebellion and call to repent", +"The mountain of the LORD's house", +"Judgment on Jerusalem's leaders", +"The Branch and the remnant", +"The vineyard song and the woes", +"Isaiah's vision and call", +"The sign of Immanuel", +"Assyria is coming", +"Unto us a child is born", +"Judgment on Assyria", +"The branch of Jesse", +"Songs of salvation", +"The doom of Babylon", +"The fall of the king of Babylon", +"Judgment on Moab", +"Moab's pride and plight", +"Judgment on Damascus", +"Woe to the land of Ethiopia", +"Prophecy against Egypt", +"Isaiah's sign against Egypt and Ethiopia", +"The fall of Babylon foreseen", +"The valley of vision", +"Judgment on Tyre", +"The earth laid waste", +"A feast on the mountain", +"Judah's song of salvation", +"Israel's restoration", +"Woe to Ephraim's drunkards", +"Woe to Ariel", +"Woe to those who trust in Egypt", +"Egypt is no help", +"The righteous king", +"Woe to the destroyer; the LORD arises", +"Judgment on the nations and Edom", +"The glory of the redeemed", +"Sennacherib invades Judah", +"Hezekiah prays", +"Hezekiah's illness", +"The Babylonian envoys", +"Comfort ye my people", +"Fear not, for I am with thee", +"Behold my servant", +"Thou art mine", +"The folly of idols", +"Cyrus named as deliverer", +"Babylon's idols fall", +"Babylon's pride and fall", +"Stubborn Israel", +"The servant's mission to the nations", +"The servant's obedience", +"The LORD comforts Zion", +"Awake; good tidings of peace", +"The suffering servant", +"Sing, O barren", +"Ho, every one that thirsts", +"Salvation for the foreigner", +"The idolaters and the contrite", +"True fasting", +"Sin separates from God", +"Arise, shine; Zion's glory", +"The anointed one's good news", +"Zion's new name", +"The day of vengeance", +"A prayer for mercy", +"New heavens and a new earth promised", +"Final judgment and glory" +], +"Jeremiah": [ +"Jeremiah's call", +"Israel's backsliding", +"Return, O backsliding Israel", +"Disaster from the north", +"Judah's corruption", +"Siege of Jerusalem foretold", +"The temple sermon", +"Judgment and the wounded people", +"The weeping prophet", +"The folly of idols", +"The broken covenant", +"Jeremiah's complaint", +"The linen girdle and the exile", +"Drought and prayer", +"The LORD's refusal and Jeremiah's lament", +"Jeremiah forbidden to marry", +"Judah's sin and the sabbath", +"The potter's house", +"The broken bottle", +"Jeremiah in the stocks", +"Zedekiah's inquiry", +"Judgment on the kings", +"The righteous Branch; false prophets", +"The vision of two baskets of figs", +"Seventy years of captivity; the cup of wrath", +"Jeremiah's life threatened", +"The yoke of Babylon", +"The false prophet Hananiah", +"Letter to the exiles", +"The promise of restoration", +"The new covenant", +"The purchase of a field", +"The promise of the Branch", +"The warning to Zedekiah", +"The Rechabites", +"Baruch reads the scroll; the king burns it", +"Jeremiah in prison", +"Jeremiah in the cistern", +"Jerusalem falls", +"Gedaliah made governor", +"Gedaliah murdered", +"The remnant warned not to go to Egypt", +"The remnant goes to Egypt", +"Idolatry in Egypt condemned", +"A promise to Baruch", +"Prophecy against Egypt", +"Prophecy against Philistia", +"Prophecy against Moab", +"Prophecy against Ammon, Edom and Damascus", +"Prophecy against Babylon", +"Babylon's fall and Israel's return", +"The fall of Jerusalem recounted" +], +"Lamentations": [ +"Jerusalem weeps in her solitude", +"The LORD's anger", +"Hope in the LORD's mercies", +"The siege and its horrors", +"A prayer for restoration" +], +"Ezekiel": [ +"Ezekiel's vision of the glory", +"Ezekiel commissioned", +"The watchman and the scroll", +"The siege of Jerusalem acted out", +"The sword and the scattering", +"Judgment on the idolatrous mountains", +"The end has come", +"Vision of the temple's abominations", +"The slaughter of the idolaters", +"The glory departs the temple", +"Judgment on Jerusalem's leaders", +"The exile's sign", +"Judgment on false prophets", +"Idolaters rebuked", +"The useless vine", +"Jerusalem the unfaithful bride", +"Parable of the eagles and the vine", +"Each soul answers for itself", +"A lament for Israel's princes", +"Israel's rebellion recounted", +"Babylon's sword against Jerusalem", +"Jerusalem's sins", +"Two sisters: Aholah and Aholibah", +"The boiling pot; Ezekiel's wife dies", +"Judgment on Ammon, Moab, Edom", +"Prophecy against Tyre", +"Lament over Tyre", +"Judgment on the prince of Tyre", +"Judgment on Egypt", +"Egypt's fall", +"Pharaoh the fallen cedar", +"A lament for Pharaoh", +"The watchman's duty renewed", +"The false shepherds", +"Judgment on Edom", +"The mountains of Israel restored", +"The valley of dry bones; two sticks made one", +"Gog and Magog", +"Gog's destruction", +"Vision of the new temple", +"The temple's interior", +"The priests' chambers", +"The glory returns to the temple", +"The sanctuary and the priests", +"The land and the offerings", +"The prince and the offerings", +"The river from the temple", +"The division of the land" +], +"Daniel": [ +"Daniel and his friends in Babylon", +"Nebuchadnezzar's dream of the image", +"The fiery furnace", +"Nebuchadnezzar's tree dream and madness", +"Belshazzar's feast; the writing on the wall", +"Daniel in the lions' den", +"Vision of the four beasts", +"Vision of the ram and goat", +"Daniel's prayer and the seventy weeks", +"Vision of the glorious man", +"The kings of the north and south", +"The time of the end" +], +"Hosea": [ +"Hosea marries Gomer", +"Israel's unfaithfulness and restoration", +"Hosea redeems his wife", +"The LORD's controversy with Israel", +"Israel's priests and idolatry", +"Return to the LORD", +"Israel's sins", +"Israel sows the wind", +"Israel's days of punishment", +"Israel's vine and calves", +"God's love for Ephraim", +"Ephraim's deceit", +"Ephraim's idolatry and death", +"Return to the LORD and be healed" +], +"Joel": [ +"A plague of locusts", +"The day of the LORD; return to Him", +"The Spirit poured out; judgment on the nations" +], +"Amos": [ +"Judgment on Israel's neighbors", +"Judgment on Judah and Israel", +"Why the LORD judges Israel", +"Israel refuses to return", +"Seek the LORD and live", +"Woe to those at ease in Zion", +"Visions of locusts, fire and the plumb line", +"Vision of the summer fruit", +"Vision of the LORD at the altar; Israel restored" +], +"Obadiah": [ +"Edom's pride and doom; Israel restored" +], +"Jonah": [ +"Jonah flees and is thrown overboard", +"Jonah's prayer from the fish", +"Nineveh repents", +"Jonah's anger and the gourd" +], +"Micah": [ +"Judgment on Samaria and Judah", +"Woe to the oppressors", +"Judgment on Israel's rulers", +"The mountain of the LORD; Bethlehem foretold", +"The ruler from Bethlehem", +"The LORD's controversy; what He requires", +"Israel's misery and the LORD's pardon" +], +"Nahum": [ +"The LORD avenges and Nineveh falls", +"The siege and sacking of Nineveh", +"Woe to the bloody city" +], +"Habakkuk": [ +"Habakkuk's complaint about injustice", +"The just shall live by faith; woes on the proud", +"Habakkuk's prayer and faith in God" +], +"Zephaniah": [ +"The great day of the LORD", +"Seek righteousness; judgment on the nations", +"Judgment and joy: the LORD rejoices over Zion" +], +"Haggai": [ +"Consider your ways; build the house", +"The glory of the latter house" +], +"Zechariah": [ +"Return to the LORD; vision of the horsemen", +"Vision of the horns and the measuring line", +"Joshua cleansed; the Branch", +"Vision of the lampstand and olive trees", +"Vision of the flying scroll and ephah", +"Vision of the four chariots; the crown", +"Hypocritical fasting rebuked", +"The LORD will dwell in Jerusalem", +"Zion's king comes riding a donkey", +"Judgment on false shepherds; Judah strengthened", +"The good shepherd rejected", +"Jerusalem delivered; mourning for the pierced one", +"A fountain for cleansing; shepherd struck", +"The LORD comes; the kingdom is the LORD's" +], +"Malachi": [ +"Israel's offerings are despised", +"The priests rebuked; unfaithfulness", +"The messenger of the covenant; tithes withheld", +"The day of the LORD; remember the law and Elijah" +], +"Matthew": [ +"The genealogy and birth of Jesus", +"The wise men; flight to Egypt", +"John the Baptist; Jesus baptized", +"Jesus tempted; the first disciples called", +"The Sermon on the Mount: the Beatitudes", +"Giving, prayer and treasure in heaven", +"Do not judge; the house on the rock", +"Jesus heals many and calms the storm", +"Matthew called; the paralytic healed", +"The twelve sent out", +"John's question; woes on the cities", +"Lord of the sabbath; a house divided", +"The sower and the kingdom parables", +"The death of John the Baptist; five thousand fed", +"Traditions and the Canaanite woman", +"Peter's confession of Christ", +"The transfiguration", +"The unforgiving servant; forgiveness", +"Marriage and the rich young man", +"The laborers in the vineyard", +"The triumphal entry; the temple cleansed", +"The wedding feast; the great commandment", +"Woes on the scribes and Pharisees", +"The Olivet discourse", +"The ten virgins, the talents, the sheep and the goats", +"The Last Supper and Gethsemane", +"The trial before Pilate and the crucifixion", +"The resurrection and the great commission" +], +"Mark": [ +"John the Baptist; Jesus baptized and tempted", +"Jesus heals the paralytic; Levi called", +"Lord of the sabbath; the twelve appointed", +"Parable of the sower; the storm calmed", +"The Gadarene demoniac; Jairus' daughter", +"Jesus rejected at Nazareth; the twelve sent; five thousand fed", +"Traditions of men; the Syrophoenician woman", +"The four thousand; Peter's confession", +"The transfiguration; the faithless generation", +"Teaching on divorce; the rich young man", +"The triumphal entry; the fig tree and the temple", +"The vineyard parable; the great commandment", +"The Olivet discourse", +"The anointing; the Last Supper; Gethsemane", +"The trial and crucifixion", +"The resurrection and ascension" +], +"Luke": [ +"The births of John and Jesus foretold", +"Jesus is born; the boy in the temple", +"John the Baptist; Jesus' genealogy", +"The temptation; Jesus at Nazareth", +"Calling of the first disciples", +"The Sermon on the Plain", +"The centurion's servant; the widow's son", +"The sower; the storm calmed", +"Mission of the twelve; Peter's confession", +"The seventy sent; the good Samaritan", +"The Lord's prayer; woes on Pharisees", +"Beware of covetousness; be ready", +"Repent or perish; the narrow door", +"The great supper; counting the cost", +"The lost sheep, coin and son", +"The unjust steward; the rich man and Lazarus", +"Forgiveness, faith and the kingdom's coming", +"The persistent widow; the Pharisee and the publican", +"Zacchaeus; the ten pounds; the triumphal entry", +"Questions in the temple; the vineyard parable", +"The Olivet discourse", +"The Last Supper and Peter's denial foretold", +"The trial and crucifixion", +"The resurrection and the road to Emmaus" +], +"John": [ +"The Word became flesh", +"The wedding at Cana; the temple cleansed", +"Jesus and Nicodemus: ye must be born again", +"The woman at the well", +"Healing at Bethesda; the Father's witness", +"Feeding the five thousand; the bread of life", +"The feast of tabernacles", +"The woman caught in adultery; before Abraham was, I am", +"The man born blind", +"The good shepherd", +"The raising of Lazarus", +"The anointing; the triumphal entry", +"Jesus washes the disciples' feet", +"I am the way, the truth and the life", +"I am the true vine", +"The Comforter promised", +"The high priestly prayer", +"The arrest and trial before Pilate", +"The crucifixion and burial", +"The resurrection", +"Jesus appears at the sea of Tiberias" +], +"Acts": [ +"The ascension; Matthias chosen", +"Pentecost and Peter's sermon", +"The lame man healed", +"Peter and John before the council", +"Ananias and Sapphira", +"The seven deacons chosen", +"Stephen's defense and martyrdom", +"Saul persecutes the church; Philip and the Ethiopian", +"Saul's conversion", +"Peter and Cornelius", +"Peter explains; the church at Antioch", +"Herod kills James; Peter freed", +"Paul and Barnabas sent out", +"Paul at Iconium and Lystra", +"The Jerusalem council", +"Paul and Silas in Philippi", +"Paul in Thessalonica and Athens", +"Paul in Corinth", +"Paul in Ephesus", +"Paul's farewell at Miletus", +"Paul goes to Jerusalem and is arrested", +"Paul defends himself", +"Paul before the council; the plot", +"Paul before Felix", +"Paul before Festus", +"Paul before Agrippa", +"Paul's voyage and shipwreck", +"Paul in Malta and Rome" +], +"Romans": [ +"The gospel of God; all are guilty", +"The judgment of God; Jew and Gentile", +"All have sinned", +"Abraham justified by faith", +"Peace with God through faith", +"Dead to sin, alive in Christ", +"Freed from sin; the law and sin", +"Walking in the Spirit", +"Israel's election and rejection", +"Salvation to all who call", +"God has not cast off Israel", +"A living sacrifice; spiritual gifts", +"Obey authority; love fulfills the law", +"Do not judge your brother", +"Pleasing others; Paul's ministry", +"Greetings and final warnings" +], +"1 Corinthians": [ +"Divisions; the wisdom of the cross", +"God's wisdom revealed by the Spirit", +"Divisions and God's laborers", +"Servants of Christ; Paul's example", +"Immorality in the church", +"Lawsuits and the body as a temple", +"Marriage and singleness", +"Food offered to idols", +"Paul's example and the race", +"Warnings from Israel's history", +"Head coverings; the Lord's supper", +"Spiritual gifts and one body", +"Love: the greatest gift", +"Tongues and prophecy", +"The resurrection of Christ and of believers", +"The collection and final greetings" +], +"2 Corinthians": [ +"God of all comfort", +"Forgiving the repentant offender", +"The ministers of the new covenant", +"Treasure in earthen vessels", +"Our heavenly dwelling; the ministry of reconciliation", +"Paul's sufferings and exhortation", +"Godly sorrow produces repentance", +"Generous giving of the Macedonians", +"Sowing bountifully", +"Paul's authority defended", +"Paul's sufferings and boasting", +"The thorn in the flesh", +"Final warnings and greetings" +], +"Galatians": [ +"Another gospel is no gospel", +"Justified by faith, not works", +"Faith before the law; sons of God", +"Slaves to law, sons by promise", +"Stand fast in Christian liberty", +"Bear one another's burdens; sow to the Spirit" +], +"Ephesians": [ +"Blessings in Christ; Paul's prayer", +"Saved by grace through faith", +"The mystery of the Gentiles; Paul's prayer", +"Unity of the Spirit; the gifts of Christ", +"Walk in love and light", +"Husbands and wives; the whole armor of God" +], +"Philippians": [ +"Paul's joy in prison; to live is Christ", +"The mind of Christ; shine as lights", +"Paul counts all things loss", +"Rejoice in the Lord; thankful giving" +], +"Colossians": [ +"Christ the image of the invisible God", +"Complete in Christ; beware of philosophy", +"Seek those things which are above", +"Masters and servants; final greetings" +], +"1 Thessalonians": [ +"The Thessalonians' faith and example", +"Paul's ministry among them", +"Timothy's encouraging report", +"Live to please God; the dead in Christ", +"The day of the Lord and final exhortations" +], +"2 Thessalonians": [ +"Comfort in persecution; the Lord's return", +"The man of sin revealed", +"Pray for us; work and do not be idle" +], +"1 Timothy": [ +"Beware of false doctrine; Paul's testimony", +"Pray for all men; roles in worship", +"Qualifications for bishops and deacons", +"Warnings of apostasy; be an example", +"Care for widows and elders", +"Godliness with contentment; fight the good fight" +], +"2 Timothy": [ +"Stir up the gift; be not ashamed", +"Be strong; a good soldier of Christ", +"Perilous times; all scripture is God-breathed", +"Preach the word; Paul's final charge" +], +"Titus": [ +"Qualifications of elders; rebuke false teachers", +"Sound doctrine for every group", +"Good works; avoid foolish disputes" +], +"Philemon": [ +"Paul pleads for Onesimus" +], +"Hebrews": [ +"Christ is greater than the angels", +"Christ made like us to save us", +"Christ greater than Moses", +"Enter into God's rest", +"Christ our high priest", +"Press on to maturity; the anchor of hope", +"Melchizedek's priesthood", +"Christ mediator of a better covenant", +"The earthly and heavenly tabernacle", +"One sacrifice for sins forever", +"By faith", +"Run the race; God's discipline", +"Final exhortations and blessings" +], +"James": [ +"Count it joy; be doers of the word", +"Faith without works is dead", +"The tongue is a fire; true wisdom", +"Submit to God; do not boast of tomorrow", +"Be patient; the prayer of faith" +], +"1 Peter": [ +"A living hope; be holy", +"A chosen generation; submit and live honorably", +"Husbands and wives; suffering for righteousness", +"Live for God's will; stewards of grace", +"Elders shepherd the flock; be sober and vigilant" +], +"2 Peter": [ +"Add to your faith; certain prophecy", +"False teachers will come", +"The day of the Lord will come as a thief" +], +"1 John": [ +"God is light; fellowship", +"Do not love the world; antichrists", +"We are God's children now", +"Love one another", +"Faith that overcomes the world; eternal life" +], +"2 John": [ +"Walk in truth and love; beware deceivers" +], +"3 John": [ +"Hospitality to the brethren; Diotrephes and Demetrius" +], +"Jude": [ +"Contend for the faith against ungodly men" +], +"Revelation": [ +"The vision of Christ among the lampstands", +"Letters to Ephesus, Smyrna, Pergamos, Thyatira", +"Letters to Sardis, Philadelphia, Laodicea", +"The throne in heaven", +"The sealed book and the Lamb", +"The first six seals", +"The 144,000 sealed; the multitude in white", +"The seventh seal and the first four trumpets", +"The fifth and sixth trumpets", +"The angel and the little book", +"The two witnesses; the seventh trumpet", +"The woman and the dragon", +"The beast from the sea and the beast from the earth", +"The Lamb on Zion; the harvest", +"The seven last plagues", +"The seven bowls of wrath", +"The great harlot of Babylon", +"The fall of Babylon", +"The marriage supper; Christ returns", +"The thousand years; the great white throne", +"The new heaven and new earth", +"The river of life; Christ is coming" +] +} \ No newline at end of file diff --git a/doc/openapi.yaml b/doc/openapi.yaml index bb92aff..ad8e54c 100644 --- a/doc/openapi.yaml +++ b/doc/openapi.yaml @@ -5,7 +5,7 @@ info: version: 1.0.0 servers: - - url: https://api.1611.social + - url: https://bible.nicecrew.digital # Add security definitions for authorization tokens security: diff --git a/html/chapter_nav.html b/html/chapter_nav.html new file mode 100644 index 0000000..0bf4dba --- /dev/null +++ b/html/chapter_nav.html @@ -0,0 +1,5 @@ + diff --git a/html/dictionary.html b/html/dictionary.html index 6ba6043..e6d2138 100644 --- a/html/dictionary.html +++ b/html/dictionary.html @@ -4,22 +4,22 @@ Bible Dictionary - KJV API - - +
-Home | Dictionary | -Random Verse | Verse of the Day +Home Dictionary +Random Verse Verse of the Day

Bible Dictionary

- - - - Rich - Plain - JSON + + +
View
+ + + +

This tool uses an abridged 1828 Noah Webster dictionary.

diff --git a/html/find.html b/html/find.html index 3a8ac13..3c6888e 100644 --- a/html/find.html +++ b/html/find.html @@ -4,19 +4,19 @@ Quick Search - KJV API - - + +
-Home | Quick Search | -Random Verse | +Home Quick Search +Random Verse Verse of the Day

Quick Search

- - + +
diff --git a/html/heaven.html b/html/heaven.html index c910998..38f7d35 100644 --- a/html/heaven.html +++ b/html/heaven.html @@ -1,3 +1,17 @@ + + + + + +How to Be Saved - KJV API + + + +
+Home Quick Search +Random Verse Verse of the Day +
+

How to Be Saved

1. Understand you are a sinner:

No one is good enough to go to Heaven on their own merit. No matter how much good we do, we still come short.

Romans 3:23(a) For all have sinned, and come short of the glory of God;

@@ -28,3 +42,5 @@

Let us help you word a prayer. Realize it is not mere words that save, but your faith in Jesus Christ.

Dear Jesus, I know that I am a sinner, and I know I deserve to go to Hell. But I believe that you died on the cross, was buried, and rose from the grave to pay for my sins. Please save me and take me to Heaven when I die. Amen.

+ + diff --git a/html/index.html b/html/index.html index ef78ff6..74b9730 100644 --- a/html/index.html +++ b/html/index.html @@ -5,114 +5,77 @@ KJV API - Legacy Edition - - + + +
-

KJV API (Legacy edition)

+

KJV API (Legacy edition)

+

King James Bible: search, read, and query by reference.

-
-
-

- - + + +
+ + + +
@@ -171,8 +260,9 @@ {{ book.title }}
+ {% if book.about %}

{{ book.about }}

{% endif %} {% for chapter in range(1, book.chapters + 1) %} - {{ chapter }} + {{ chapter }}{% if book.summaries|length >= chapter %}{{ book.summaries[chapter - 1] }}{% endif %} {% endfor %}
@@ -191,8 +281,9 @@ {{ book.title }}
+ {% if book.about %}

{{ book.about }}

{% endif %} {% for chapter in range(1, book.chapters + 1) %} - {{ chapter }} + {{ chapter }}{% if book.summaries|length >= chapter %}{{ book.summaries[chapter - 1] }}{% endif %} {% endfor %}
@@ -201,8 +292,8 @@ {% endfor %} - -
+
+ API Documentation
API ENDPOINT SPECIFICATION v 0.3
 ================================
 
@@ -275,9 +366,19 @@ api.tld/show?kw=john3:16,1john5:7&view=plain
 BUGS
 ====
 Suggestions, bug reports or improvements can be reported to @tyler@nicecrew.digital
-
+
+ diff --git a/html/navbar.html b/html/navbar.html index b653ba5..29eb451 100644 --- a/html/navbar.html +++ b/html/navbar.html @@ -1,28 +1,35 @@ -
- -
- -
- - +
+ + +
+{chapter_nav} + diff --git a/html/plan.html b/html/plan.html new file mode 100644 index 0000000..ae66ca7 --- /dev/null +++ b/html/plan.html @@ -0,0 +1,98 @@ + + + + + +Make a Bible Reading Plan - KJV API + + + + + +
+Home Quick Search +Random Verse Verse of the Day +Saved +
+

Make a Bible Reading Plan

+ +
+ You already have a plan in this browser: continue reading. +
+ +

Pick where to start and how much to read each day. You'll get a page to bookmark; open it +each day to see today's reading, press Done when you've read it, and the plan moves on. +You can always go back to the previous reading or jump to any verse.

+ +
+
+ +
+ + + + +
+ +
+ +
+ How much each day +
+ +
+ + +
+ +

Your plan is saved on this server under a short code in the page address, so the bookmark +is all you need. Plans unused for 90 days are removed.

+ + + + diff --git a/html/reader.html b/html/reader.html index d38356c..c74e560 100644 --- a/html/reader.html +++ b/html/reader.html @@ -1,7 +1,24 @@ + + + + + +Books - KJV API + + + +
+Home Quick Search +
+

Books

+
{% for book in books %} {% endfor %} -
{{ book[0] }}
\ No newline at end of file + +
+ + diff --git a/html/reading_plan.html b/html/reading_plan.html deleted file mode 100644 index 85de282..0000000 --- a/html/reading_plan.html +++ /dev/null @@ -1,5 +0,0 @@ -
- - - -
\ No newline at end of file diff --git a/html/response.html b/html/response.html index ac3061c..b9e4c04 100644 --- a/html/response.html +++ b/html/response.html @@ -1,4 +1,4 @@ -
+
{text}
diff --git a/html/saved.html b/html/saved.html new file mode 100644 index 0000000..122100b --- /dev/null +++ b/html/saved.html @@ -0,0 +1,97 @@ + + + + + +Saved - KJV API + + + + +
+Home Quick Search Reading Plan +Random Verse Verse of the Day +
+

Saved

+

Saved in this browser only. Clearing site data or using another device starts fresh.

+ +

Favorite verses

+
+

Definitions

+
+

Cross references

+
+ + + + diff --git a/html/search.html b/html/search.html index 596071a..80ae189 100644 --- a/html/search.html +++ b/html/search.html @@ -4,22 +4,22 @@ Bible Search - KJV API - - +
-Home | Quick Search | Bible Search | -Random Verse | Verse of the Day +Home Quick Search Bible Search +Random Verse Verse of the Day

Bible Search

- - - - Rich - Plain - JSON + + +
View
+ + + +

diff --git a/html/seq.html b/html/seq.html index cfae26f..07bf55d 100644 --- a/html/seq.html +++ b/html/seq.html @@ -4,13 +4,12 @@ Sequential Bible Viewer - KJV API - - +
-Home | Quick Search | Sequential Viewer | -Random Verse | Verse of the Day +Home Quick Search Sequential Viewer +Random Verse Verse of the Day

Sequential Bible Viewer

In order to use this, you first need a UID, which you can get by querying /seq?getuid=true and optionally include &start=BIBLEREF (where BIBLEREF is like 1Cor2:1) to set a start position other than Genesis 1:1.

@@ -18,7 +17,7 @@

The &view= argument works with this.

Example Pleroma Bot shell script

#!/bin/sh
-verse=$(curl -XGET https://api.1611.social/seq?uid=MYUID?view=plain);
+verse=$(curl -XGET https://bible.nicecrew.digital/seq?uid=MYUID?view=plain);
 curl -XPOST -u biblebot:MYPASSWORD \
  -H "Content-Type: application/json" \
  -d '{"status": "'"${verse}"'"}' \
diff --git a/html/seq_report.html b/html/seq_report.html
index 18d8a28..4f3f6dc 100644
--- a/html/seq_report.html
+++ b/html/seq_report.html
@@ -1,3 +1,17 @@
+
+
+
+
+
+Sequence Report - KJV API
+
+
+
+
+Home Quick Search +
+

Sequence Report

+
{% for uid in uids %} @@ -6,4 +20,7 @@ {% endfor %} -
{{ uid[2] }}
\ No newline at end of file + +
+ + diff --git a/html/shortcode_report.html b/html/shortcode_report.html index 2862337..a8a3390 100644 --- a/html/shortcode_report.html +++ b/html/shortcode_report.html @@ -1,14 +1,31 @@ + + + + + +Shortcode Report - KJV API + + + +
+Home Quick Search +
+

Shortcode Report

+
- - + + {% for code in shortcodes %} - - + + {% endfor %} -
shortcode |query |shortcodequery creation_date
{{ code[0] }} |{{ code[1] }} |{{ code[0] }}{{ code[1] }} {{ code[2] }}
\ No newline at end of file + +
+ + diff --git a/html/show.html b/html/show.html index 1598769..e83053a 100644 --- a/html/show.html +++ b/html/show.html @@ -4,22 +4,23 @@ Show Verse - KJV API - - + +
-Home | Quick Search | Show Verse | -Random Verse | Verse of the Day +Home Quick Search Show Verse +Random Verse Verse of the Day

Show Verse

- - - - Rich - Plain - JSON + + +
View
+ + + +

diff --git a/js/book_suggest.js b/js/book_suggest.js new file mode 100644 index 0000000..ead0cda --- /dev/null +++ b/js/book_suggest.js @@ -0,0 +1,160 @@ +// Book name suggestions for reference/search boxes: type "joh" and get +// John, 1 John...; Tab (or tap/click) completes. Only offered while the text +// could still be the start of a book name, so keyword searches are unaffected. +(function () { + var BOOKS = __BOOKS__; + var MAX_SHOWN = 8; + var list = null; + var current = { input: null, matches: [], active: 0, explicit: false }; + + function norm(text) { + return text.toLowerCase().replace(/[^a-z0-9]/g, ""); + } + + function injectStyle() { + var style = document.createElement("style"); + style.textContent = + ".book-suggest{position:absolute;z-index:2000;margin:4px 0 0;padding:4px 0;list-style:none;" + + "background:var(--color-bg-secondary,#fff);color:var(--color-text-primary,#111);" + + "border:1px solid var(--color-border,#ccc);border-radius:6px;" + + "box-shadow:0 6px 18px var(--color-shadow,rgba(0,0,0,.25));font:1rem var(--font-ui,sans-serif)}" + + ".book-suggest li{padding:10px 14px;cursor:pointer;min-height:44px;display:flex;align-items:center}" + + ".book-suggest li[aria-selected=true]{background:var(--color-accent-primary,#2b5c94);" + + "color:var(--color-on-accent,#fff)}" + + ".book-suggest .hint{min-height:0;padding:4px 14px;font-size:.8rem;cursor:default;" + + "color:var(--color-text-muted,#666);background:none}" + + "@media (hover:none){.book-suggest .hint{display:none}}"; + document.head.appendChild(style); + } + + function ensureList() { + if (list) { + return list; + } + injectStyle(); + list = document.createElement("ul"); + list.className = "book-suggest"; + list.id = "book-suggest-list"; + list.setAttribute("role", "listbox"); + list.hidden = true; + // keep focus in the input while clicking an item + list.addEventListener("mousedown", function (event) { event.preventDefault(); }); + document.body.appendChild(list); + return list; + } + + function close() { + if (list) { + list.hidden = true; + } + if (current.input) { + current.input.setAttribute("aria-expanded", "false"); + current.input.removeAttribute("aria-activedescendant"); + } + current.matches = []; + } + + function accept(book) { + var input = current.input; + input.value = book + " "; + input.setSelectionRange(input.value.length, input.value.length); + close(); + input.focus(); + } + + function paint() { + var items = list.querySelectorAll("li[role=option]"); + for (var i = 0; i < items.length; i++) { + var selected = i === current.active; + items[i].setAttribute("aria-selected", selected ? "true" : "false"); + if (selected) { + current.input.setAttribute("aria-activedescendant", items[i].id); + } + } + } + + function show(input, matches) { + var box = input.getBoundingClientRect(); + ensureList(); + list.textContent = ""; + matches.forEach(function (book, i) { + var item = document.createElement("li"); + item.setAttribute("role", "option"); + item.id = "book-suggest-" + i; + item.textContent = book; + item.addEventListener("click", function () { accept(book); }); + list.appendChild(item); + }); + var hint = document.createElement("li"); + hint.className = "hint"; + hint.textContent = "Tab to complete"; + list.appendChild(hint); + list.style.left = box.left + window.pageXOffset + "px"; + list.style.top = box.bottom + window.pageYOffset + "px"; + list.style.minWidth = Math.min(box.width, 320) + "px"; + list.hidden = false; + input.setAttribute("aria-expanded", "true"); + paint(); + } + + function update(input) { + current.input = input; + var value = input.value.replace(/^\s+/, ""); + // "1 jo", "song of sol": optional leading book number, then letters only + var n = /^[1-3]?\s?[A-Za-z ]*$/.test(value) ? norm(value) : ""; + var matches = n ? BOOKS.filter(function (b) { return norm(b).indexOf(n) === 0; }) : []; + // already typed a whole book name: nothing left to complete + if (matches.length === 1 && norm(matches[0]) === n) { + matches = []; + } + matches = matches.slice(0, MAX_SHOWN); + current.matches = matches; + current.active = 0; + current.explicit = false; + if (matches.length) { + show(input, matches); + } else { + close(); + } + } + + function onKeydown(event) { + var open = list && !list.hidden && current.matches.length; + if (!open) { + return; + } + var count = current.matches.length; + if (event.key === "Tab" && !event.shiftKey) { + event.preventDefault(); + accept(current.matches[current.active]); + } else if (event.key === "ArrowDown" || event.key === "ArrowUp") { + event.preventDefault(); + current.active = (current.active + (event.key === "ArrowDown" ? 1 : count - 1)) % count; + current.explicit = true; + paint(); + } else if (event.key === "Enter" && current.explicit) { + event.preventDefault(); // pick the highlighted book instead of submitting + accept(current.matches[current.active]); + } else if (event.key === "Escape") { + close(); + } + } + + function attach(input) { + input.setAttribute("autocomplete", "off"); + input.setAttribute("aria-autocomplete", "list"); + input.setAttribute("aria-controls", "book-suggest-list"); + input.addEventListener("input", function () { update(input); }); + input.addEventListener("keydown", function (event) { + current.input = input; + onKeydown(event); + }); + input.addEventListener("blur", close); + } + + var inputs = document.querySelectorAll("input[data-book-suggest]"); + for (var i = 0; i < inputs.length; i++) { + attach(inputs[i]); + } + window.addEventListener("resize", close); +})(); diff --git a/js/collapsible.js b/js/collapsible.js index f17bb8e..78a2d0a 100644 --- a/js/collapsible.js +++ b/js/collapsible.js @@ -1,10 +1,28 @@ var coll = document.getElementsByClassName("collapsible"); var i; +function setOpen(button, open) { + var content = button.nextElementSibling; + button.classList.toggle("active", open); + button.setAttribute("aria-expanded", open ? "true" : "false"); + if (!content) { + return; + } + if (!open) { + content.style.display = "none"; + } else if (content.classList.contains('chapter-grid')) { + content.style.display = "grid"; + } else { + content.style.display = "block"; + } +} + for (i = 0; i < coll.length; i++) { + coll[i].setAttribute("aria-expanded", "false"); coll[i].addEventListener("click", function() { var content = this.nextElementSibling; - var isCurrentlyOpen = content.style.display === "block" || content.style.display === "grid"; + // computed style, so content hidden by a stylesheet counts as closed + var isCurrentlyOpen = content && getComputedStyle(content).display !== "none"; // If this collapsible has a parent with book-grid class, close all siblings in that grid var parentGrid = this.closest('.book-grid'); @@ -12,25 +30,11 @@ for (i = 0; i < coll.length; i++) { var siblings = parentGrid.querySelectorAll('.collapsible'); for (var j = 0; j < siblings.length; j++) { if (siblings[j] !== this) { - siblings[j].classList.remove("active"); - var siblingContent = siblings[j].nextElementSibling; - if (siblingContent) { - siblingContent.style.display = "none"; - } + setOpen(siblings[j], false); } } } - // Toggle current element - this.classList.toggle("active"); - if (isCurrentlyOpen) { - content.style.display = "none"; - } else { - if (content.classList.contains('chapter-grid')) { - content.style.display = "grid"; - } else { - content.style.display = "block"; - } - } + setOpen(this, !isCurrentlyOpen); }); } diff --git a/js/verse_menu.js b/js/verse_menu.js new file mode 100644 index 0000000..4a406c0 --- /dev/null +++ b/js/verse_menu.js @@ -0,0 +1,265 @@ +// Tap a verse to open a small menu: favorite it, look up the 1828 Webster +// definition of its words, or see its cross references. Everything saved +// lives in this browser's localStorage (read back on the /saved page). +(function () { + var KEYS = { fav: "kjv.favorites", defs: "kjv.definitions", xrefs: "kjv.crossrefs" }; + var open = null; // { box, menu } + + function load(key) { + try { + var value = JSON.parse(localStorage.getItem(key)); + return Array.isArray(value) ? value : []; + } catch (e) { + return []; + } + } + + function save(key, list) { + try { + localStorage.setItem(key, JSON.stringify(list)); + return true; + } catch (e) { + return false; + } + } + + function indexOf(list, match) { + for (var i = 0; i < list.length; i++) { + if (match(list[i])) { + return i; + } + } + return -1; + } + + // Adds the item if it is not stored yet, removes it if it is. Returns the new "saved" state. + function toggle(key, item, match) { + var list = load(key); + var at = indexOf(list, match); + if (at >= 0) { + list.splice(at, 1); + } else { + item.t = Date.now(); + list.unshift(item); + } + save(key, list); + return at < 0; + } + + function el(tag, className, text) { + var node = document.createElement(tag); + if (className) { + node.className = className; + } + if (text !== undefined) { + node.textContent = text; + } + return node; + } + + function verseInfo(box) { + var link = box.querySelector(".verse-reference a"); + var text = box.querySelector(".verse-text"); + return { + ref: link ? link.textContent.trim() : "", + text: text ? text.textContent.trim() : "", + id: box.getAttribute("data-verse-id") || "" + }; + } + + function findUrl(ref) { + return "/find?kw=" + encodeURIComponent(ref) + "&view=rich"; + } + + function markFavorites(root) { + var favs = load(KEYS.fav); + var boxes = (root || document).querySelectorAll(".verse-container"); + for (var i = 0; i < boxes.length; i++) { + var ref = verseInfo(boxes[i]).ref; + boxes[i].classList.toggle("is-fav", indexOf(favs, function (f) { return f.ref === ref; }) >= 0); + } + } + + function closeMenu() { + if (open) { + open.box.classList.remove("selected"); + open.box.removeAttribute("aria-expanded"); + open.menu.remove(); + open = null; + } + } + + function saveButton(isSaved, onToggle) { + var button = el("button", "vm-save"); + button.type = "button"; + function paint(saved) { + button.textContent = saved ? "Saved ✓" : "Save"; + button.classList.toggle("saved", saved); + } + paint(isSaved); + button.addEventListener("click", function () { paint(onToggle()); }); + return button; + } + + function showDefinitions(info, panel) { + panel.textContent = "Looking up words…"; + var seen = {}; + var words = (info.text.toLowerCase().match(/[a-z]+/g) || []).filter(function (w) { + if (w.length < 3 || seen[w]) { + return false; + } + seen[w] = true; + return true; + }); + fetch("/defs?words=" + encodeURIComponent(words.join(","))) + .then(function (r) { return r.json(); }) + .then(function (defs) { + panel.textContent = ""; + var found = words.filter(function (w) { return defs[w]; }); + if (!found.length) { + panel.textContent = "No definitions found for the words in this verse."; + return; + } + var chips = el("div", "vm-chips"); + var detail = el("div", "vm-detail"); + detail.hidden = true; + found.forEach(function (word) { + var chip = el("button", "vm-chip", word); + chip.type = "button"; + chip.addEventListener("click", function () { + detail.hidden = false; + detail.textContent = ""; + detail.appendChild(el("strong", "", word)); + defs[word].forEach(function (d) { detail.appendChild(el("p", "", d)); }); + var match = function (s) { return s.word === word; }; + detail.appendChild(saveButton( + indexOf(load(KEYS.defs), match) >= 0, + function () { + return toggle(KEYS.defs, { word: word, defs: defs[word], ref: info.ref }, match); + } + )); + }); + chips.appendChild(chip); + }); + panel.appendChild(el("div", "vm-hint", "Tap a word for its 1828 Webster definition.")); + panel.appendChild(chips); + panel.appendChild(detail); + }) + .catch(function () { panel.textContent = "Could not load definitions."; }); + } + + function showCrossRefs(info, panel) { + if (!info.id) { + panel.textContent = "Cross references are not available here."; + return; + } + panel.textContent = "Loading cross references…"; + fetch("/xref/" + encodeURIComponent(info.id)) + .then(function (r) { return r.json(); }) + .then(function (refs) { + panel.textContent = ""; + if (!refs.length) { + panel.textContent = "No cross references for this verse."; + return; + } + refs.forEach(function (x) { + var row = el("div", "vm-xref"); + var link = el("a", "", x.ref); + link.href = findUrl(x.ref); + row.appendChild(link); + row.appendChild(el("div", "vm-xref-text", x.text)); + var match = function (s) { return s.from === info.ref && s.ref === x.ref; }; + row.appendChild(saveButton( + indexOf(load(KEYS.xrefs), match) >= 0, + function () { + return toggle(KEYS.xrefs, { from: info.ref, ref: x.ref, text: x.text }, match); + } + )); + panel.appendChild(row); + }); + }) + .catch(function () { panel.textContent = "Could not load cross references."; }); + } + + function openMenu(box) { + closeMenu(); + var info = verseInfo(box); + var menu = el("div", "verse-menu"); + menu.setAttribute("role", "group"); + menu.setAttribute("aria-label", "Actions for " + info.ref); + var actions = el("div", "vm-actions"); + var panel = el("div", "vm-panel"); + panel.hidden = true; + + var favMatch = function (f) { return f.ref === info.ref; }; + var fav = el("button", "vm-btn"); + fav.type = "button"; + function paintFav() { + var saved = indexOf(load(KEYS.fav), favMatch) >= 0; + fav.textContent = saved ? "★ Favorited" : "☆ Favorite"; + fav.classList.toggle("saved", saved); + box.classList.toggle("is-fav", saved); + } + paintFav(); + fav.addEventListener("click", function () { + toggle(KEYS.fav, { ref: info.ref, text: info.text }, favMatch); + paintFav(); + }); + + var define = el("button", "vm-btn", "Define words"); + define.type = "button"; + define.addEventListener("click", function () { + panel.hidden = false; + showDefinitions(info, panel); + }); + + var xref = el("button", "vm-btn", "Cross references"); + xref.type = "button"; + xref.addEventListener("click", function () { + panel.hidden = false; + showCrossRefs(info, panel); + }); + + var close = el("button", "vm-btn vm-close", "×"); + close.type = "button"; + close.setAttribute("aria-label", "Close menu"); + close.addEventListener("click", closeMenu); + + [fav, define, xref, close].forEach(function (b) { actions.appendChild(b); }); + menu.appendChild(actions); + menu.appendChild(panel); + box.appendChild(menu); + box.classList.add("selected"); + box.setAttribute("aria-expanded", "true"); + open = { box: box, menu: menu }; + } + + document.addEventListener("click", function (event) { + var box = event.target.closest && event.target.closest(".verse-container"); + if (!box) { + closeMenu(); + return; + } + // links, buttons and the open menu keep their own behaviour; so does selecting text + if (event.target.closest("a, button, input, .verse-menu")) { + return; + } + var selection = window.getSelection && window.getSelection().toString(); + if (selection) { + return; + } + if (open && open.box === box) { + closeMenu(); + } else { + openMenu(box); + } + }); + + document.addEventListener("keydown", function (event) { + if (event.key === "Escape") { + closeMenu(); + } + }); + + markFavorites(); +})(); diff --git a/kjv_fe.html b/kjv_fe.html index c8bb75d..8717374 100644 --- a/kjv_fe.html +++ b/kjv_fe.html @@ -3,46 +3,49 @@ - Search + Semantic Search - KJV API + -

Bible Semantic Search

-

This is NOT a keyword search. This means searching single words will not have the results you expect. For that, use the normal keyword search at api.1611.social

+
+ Home Quick Search + Random Verse Verse of the Day +
+

Bible Semantic Search

+

This is NOT a keyword search. This means searching single words will not have the results you expect. For that, use the normal keyword search at bible.nicecrew.digital

Use a sentence like "The Lord will kill his enemies without mercy" - you are supposed to search an idea, not individual words.

-

Recent searches:

+

Recent searches

    - - +
    + + +