diff --git a/app.py b/app.py index af917e5..111b837 100644 --- a/app.py +++ b/app.py @@ -97,6 +97,26 @@ if not kjv_cur: if not sermon_cur: print("WARN: Error connecting to Sermon Database") +# Build grammatical variants lookup from word groups +variant_groups = json.load(open("data/grammatical_variants.json", "r")) +grammatical_variants_map = {} +for group in variant_groups: + for word in group: + # Map each word to all other words in its group + grammatical_variants_map[word] = [w for w in group if w != word] + +# Build theological synonyms lookup from word groups +theological_synonym_groups = json.load(open("data/theological_synonyms.json", "r")) +theological_synonyms_map = {} +for group in theological_synonym_groups: + for word in group: + # Map each word to other words in its group + theological_synonyms_map[word] = [w for w in group if w != word] + +# Load concept mappings for common theological terms not in KJV +concept_mappings = json.load(open("data/concept_mappings.json", "r")) +concept_mappings_dict = {mapping["query"].lower(): mapping for mapping in concept_mappings} + cached: dict = { "verse_ids": [i[0] for i in kjv_cur.execute("SELECT id FROM fts_kjv;").fetchall()], "book_names": [ @@ -105,13 +125,21 @@ cached: dict = { "chapter_list": [], "books_chapter_lengths": {}, "json_dictionary": json.load(open("data/1828_Webster_KJV.json", "r")), + "grammatical_variants": grammatical_variants_map, + "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(), "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(), + "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(), } # Fill the chapter list with unique chapters for seeking back and forward by index. @@ -126,17 +154,52 @@ for book in range(1, 67): "SELECT c FROM fts_kjv where b = ? ORDER BY id DESC LIMIT 1;", (book,) ).fetchone()[0] +# Cache book_id to bookname mapping for fast lookups +cached["book_id_to_name"] = {} +for row in kjv_cur.execute("SELECT b, n FROM key_english").fetchall(): + cached["book_id_to_name"][row[0]] = row[1] + +# Organize books by testament and category for index page +from collections import defaultdict +testament_data = {"OT": defaultdict(list), "NT": defaultdict(list)} +for book in cached["book_info"]: + book_order, title, category, testament, num_chapters = book + testament_data[testament][category].append({ + "title": title, + "chapters": num_chapters + }) +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() + cached["html_index"] = render_template_string(template_content, testament_data=cached["book_info_for_index"]) + @app.get("/js/shortcode.js") def serve_shortcode_js(): - return Response(cached.get("js_shortcode"), mimetype='application/javascript') + response = Response(cached.get("js_shortcode"), mimetype='application/javascript') + response.headers['Cache-Control'] = 'public, max-age=31536000' + return response @app.get("/js/collapsible.js") def serve_collapsible_js(): - return Response(cached.get("js_collapsible"), mimetype='application/javascript') + response = Response(cached.get("js_collapsible"), mimetype='application/javascript') + response.headers['Cache-Control'] = 'public, max-age=31536000' + return response @app.get("/styles/page.css") def serve_page_css(): - return send_file("styles/page.css", mimetype='text/css') + response = Response(cached.get("css_page"), mimetype='text/css') + response.headers['Cache-Control'] = 'public, max-age=31536000' + return response + +@app.get("/favicon.ico") +@app.get("/favicon.svg") +def serve_favicon(): + response = Response(cached.get("favicon_svg"), mimetype='image/svg+xml') + response.headers['Cache-Control'] = 'public, max-age=31536000' + return response def generate_short_id(): remain = random.randrange(614_656, 17_210_367) # resolves to length of 5 @@ -151,11 +214,10 @@ def generate_short_id(): def lookup_bookname(book_id): - result = kjv_cur.execute( - "SELECT n FROM key_english WHERE b = ?;",(book_id,)).fetchone() - if result: - result = result[0] - return result + # Ensure book_id is an integer for cache lookup + if isinstance(book_id, str): + book_id = int(book_id) + return cached["book_id_to_name"].get(book_id) def lookup_book_id(bookname) -> int: @@ -452,34 +514,55 @@ def parse_query(query_override: str = None): return send_file("html/find.html") if query.endswith(","): query = query[:-1] - result = show(kw=query, human_readable=human_readable) - if not hasattr(result, "__len__"): - logging.debug(f"parse_query.show = '{result}'") - return result - if len(result) > 17: - return result - result = search() + # 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: + result = show(kw=query, human_readable=human_readable) + if not hasattr(result, "__len__"): + logging.debug(f"parse_query.show = '{result}'") + return result + + if len(result) > 17: + return result + # Reference parsing failed, fall back to keyword search + result = search() # Remove the closing from search() so we can add dictionary content inside .responses if result.endswith(""): result = result[:-6] # Remove last - result += "" - result += "

1828 Webster Definitions

Click on the word to get the definition.

" + + # Check if we have any dictionary definitions before showing the section split_query = query.replace(",", " ").split(" ") if len(split_query) > 20: return "Your Query is too long. 20 Words or less." + + definitions_html = "" + has_definitions = False for word in split_query: definition = define(word) if not definition: continue - result += ( + has_definitions = True + definitions_html += ( "" ) - result += "
" + definition + "
" + definitions_html += "
" + definition + "
" logging.debug(f"parse_query.define = '{word}'") - result += "" + + # Only add the dictionary section if we found at least one definition + if has_definitions: + result += "" + result += "

1828 Webster Definitions

Click on the word to get the definition.

" + result += definitions_html + result += "" + result += "" return result @@ -645,7 +728,7 @@ def response_rich(scripture): def scripture_response(scriptures, human_readable=False): response = [] - arg_view = request.args.get("view", "json") + arg_view = request.args.get("view", "rich") logging.debug(f"search ->requested_format = {arg_view}") if arg_view == "plain": for scripture in scriptures: @@ -700,7 +783,7 @@ def idx(shortcode=""): logging.info(f"/{shortcode}") return resolve_shortcode(shortcode) logging.info(f"/{shortcode}") - return send_file("html/index.html") + return cached["html_index"] @app.get("/fe") def frontend(): @@ -771,19 +854,287 @@ def search(): return send_file("html/search.html") else: return send_file("html/search.html") + + # Check if this is a common theological concept not in KJV + concept_mapping = cached["concept_mappings"].get(cleaned_keywords.lower(), None) + concept_notice = None + if concept_mapping: + # Build hyperlinks for all search terms + original_query = cleaned_keywords + search_links = [] + for term in concept_mapping["search_terms"]: + search_link = f"/search?kw={term.replace(' ', '+')}&view=rich" + search_links.append(f"{term}") + + links_html = ", ".join(search_links) + concept_notice = f"
{original_query.capitalize()}: {concept_mapping['explanation']}
You may find what you are looking for with these search terms: {links_html}
" + + # Use the first search term for the actual search + cleaned_keywords = concept_mapping["search_terms"][0] + + def score_result_by_proximity(verse_text, query_words): + """ + Score a verse based on how close together the query words appear. + Higher score = words appear closer together. + """ + verse_lower = verse_text.lower() + + # Find all positions of each query word + word_positions = {} + for word in query_words: + word_lower = word.lower() + positions = [] + start = 0 + while True: + pos = verse_lower.find(word_lower, start) + if pos == -1: + break + positions.append(pos) + start = pos + 1 + word_positions[word] = positions + + # If any word is missing, score is 0 + if any(len(positions) == 0 for positions in word_positions.values()): + return 0 + + # Calculate minimum distance between all query words + # For each combination of positions, find the span (max_pos - min_pos) + # Smaller span = higher score + import itertools + + # Get all combinations of positions for all words + all_position_combos = list(itertools.product(*word_positions.values())) + + if not all_position_combos: + return 0 + + # Find the combination with minimum span + min_span = float('inf') + for combo in all_position_combos: + span = max(combo) - min(combo) + if span < min_span: + min_span = span + + # Convert span to score (smaller span = higher score) + # Use inverse: 1000 / (span + 1) so adjacent words score ~1000, far apart score low + score = 1000.0 / (min_span + 1) + + return score + results = lookup_fts(cleaned_keywords) + + # Sort results by proximity score if we have multiple query words + if results and len(cleaned_keywords.split()) > 1: + query_words = cleaned_keywords.split() + scored_results = [] + for result in results: + verse_text = result[4] # The text is at index 4 + score = score_result_by_proximity(verse_text, query_words) + scored_results.append((score, result)) + + # Sort by score descending, then keep just the results + scored_results.sort(key=lambda x: x[0], reverse=True) + results = [result for score, result in scored_results] + if not results: + # PRIORITY 1: Try grammatical variant expansion first + words = cleaned_keywords.split() + + def sort_variants_by_similarity(original, variants): + """Sort variants by morphological similarity: forwards (longer/more specific) then backwards (shorter/simpler)""" + def get_common_prefix_length(w1, w2): + """Find how many characters match from the start""" + prefix_len = 0 + for i in range(min(len(w1), len(w2))): + if w1[i].lower() == w2[i].lower(): + prefix_len += 1 + else: + break + return prefix_len + + scored_variants = [] + for variant in variants: + prefix_len = get_common_prefix_length(original, variant) + + # Reject variants that don't share the stem (beginning doesn't match) + if prefix_len < min(2, len(original) // 2): + continue + + # Score: prioritize common prefix length (stem preservation) heavily, + # then prefer longer variants (forward/more specific) over shorter (backward/simpler) + # Prefix is weighted 1000x to ensure stem-preservation is primary + score = (prefix_len * 1000) + len(variant) + scored_variants.append((score, variant)) + + # Sort by score descending: highest score = most similar with longest form first + return [v for _, v in sorted(scored_variants, key=lambda x: x[0], reverse=True)] + + for word in words: + variants = cached["grammatical_variants"].get(word.lower(), []) + if variants: + # Sort variants: forwards in likeness (longer/specific) then backwards (shorter/simple) + sorted_variants = sort_variants_by_similarity(word.lower(), variants) + + # Try each variant in order of similarity + for variant in sorted_variants: + variant_query = cleaned_keywords.replace(word, variant, 1) + results = lookup_fts(variant_query) + if results: + response_list = scripture_response(results) + arg_view = request.args.get("view", None) + if arg_view == "plain": + response = f"Showing results for '{variant_query}' (grammatical variant of '{cleaned_keywords}')\n
" + "
".join(response_list) + elif arg_view == "rich": + variant_notice = f"
Showing results for '{variant_query}' (grammatical variant of '{cleaned_keywords}')
" + response_str = "".join(response_list) + response = response_str.replace("
", f"
{variant_notice}", 1) + else: + response = jsonify({"original": cleaned_keywords, "variant": variant_query, "results": response_list}) + return response + + # PRIORITY 2: If grammatical variants didn't help, try spell-check h = hunspell.HunSpell("data/kjv.dic", "data/kjv.aff") + corrected_words = [] + had_corrections = False + + for word in words: + if h.spell(word): + # Word is spelled correctly + corrected_words.append(word) + else: + # Word is misspelled, get suggestions + word_suggestions = h.suggest(word) + if word_suggestions: + corrected_words.append(word_suggestions[0]) + had_corrections = True + else: + # No suggestions, keep original + corrected_words.append(word) + + if had_corrections: + corrected_keyword = " ".join(corrected_words) + results = lookup_fts(corrected_keyword) + if results: + response_list = scripture_response(results) + arg_view = request.args.get("view", None) + if arg_view == "plain": + response = f"Showing results for '{corrected_keyword}' (corrected from '{cleaned_keywords}')\n
" + "
".join(response_list) + elif arg_view == "rich": + correction_notice = f"
Showing results for '{corrected_keyword}' (corrected from '{cleaned_keywords}')
" + response_str = "".join(response_list) + response = response_str.replace("
", f"
{correction_notice}", 1) + else: + response = jsonify({"corrected_from": cleaned_keywords, "corrected_to": corrected_keyword, "results": response_list}) + return response + + # PRIORITY 3: Try theological synonym expansion + for word in words: + synonyms = cached["theological_synonyms"].get(word.lower(), None) + if synonyms: + for synonym in synonyms: + synonym_query = cleaned_keywords.replace(word, synonym, 1) + results = lookup_fts(synonym_query) + if results: + response_list = scripture_response(results) + arg_view = request.args.get("view", None) + explanation = f"In the KJV, '{word}' and '{synonym}' often refer to similar theological concepts. Consider also searching for related verses." + if arg_view == "plain": + response = f"Showing results for '{synonym_query}' (theological equivalent of '{cleaned_keywords}')\n{explanation}\n
" + "
".join(response_list) + elif arg_view == "rich": + theological_notice = f"
Showing results for '{synonym_query}' (theological equivalent of '{cleaned_keywords}')
{explanation}
" + response_str = "".join(response_list) + response = response_str.replace("
", f"
{theological_notice}", 1) + else: + response = jsonify({"original": cleaned_keywords, "theological_equivalent": synonym_query, "explanation": explanation, "results": response_list}) + return response + + # PRIORITY 4: If we still have no results, show spelling suggestions suggestions = h.suggest(cleaned_keywords) - return "{} not found, try one of the following: {}".format( - cleaned_keywords, ", ".join(suggestions) - ) + arg_view = request.args.get("view", "rich") + + 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 += "
" + no_results_html += f"
No results found for '{cleaned_keywords}'.
" + + if suggestions: + no_results_html += "

Did you mean?

" + no_results_html += "

We couldn't find any verses matching your search. Here are some spelling suggestions:

" + for suggestion in suggestions[:5]: # Limit to 5 suggestions + search_link = f"/search?kw={suggestion}&view=rich" + no_results_html += f"" + no_results_html += "
" + else: + no_results_html += "

Search Tips

" + no_results_html += "

We couldn't find any verses matching your search. Try:

" + no_results_html += "
    " + no_results_html += "
  • Checking your spelling
  • " + no_results_html += "
  • Using different keywords
  • " + no_results_html += "
  • Searching for love, faith, or hope
  • " + no_results_html += "
" + + no_results_html += "
" + return no_results_html + else: + # Plain text fallback + return "{} not found, try one of the following: {}".format( + cleaned_keywords, ", ".join(suggestions) if suggestions else "No suggestions available" + ) response_list = scripture_response(results) - arg_view = request.args.get("view", None) + + # Check if any words in the search have theological synonyms to suggest + theological_suggestions = [] + words = cleaned_keywords.split() + for word in words: + synonyms = cached["theological_synonyms"].get(word.lower(), None) + if synonyms: + for synonym in synonyms: + theological_suggestions.append(synonym) + + arg_view = request.args.get("view", "rich") if arg_view == "plain": response = "
".join(response_list) + if concept_notice: + response = concept_notice + "
" + response + if theological_suggestions: + response += "

Also consider searching: " + for suggestion in theological_suggestions: + response += f"{suggestion}
" elif arg_view == "rich": response = "".join(response_list) + # Insert concept notice at the top if it exists + if concept_notice: + # Find the first
after the opening
and insert after it + responses_start = response.find("
") + if responses_start != -1: + # Find the results-count div closing + insert_pos = response.find("
", responses_start + len("
")) + if insert_pos != -1: + insert_pos += len("
") + response = response[:insert_pos] + concept_notice + response[insert_pos:] + if theological_suggestions: + # Add suggestions before the LAST closing
which closes the responses container + suggestions_html = "

Related Theological Concepts

" + suggestions_html += "

In the KJV, these terms often refer to similar theological concepts. Consider also searching for related verses.

" + for suggestion in theological_suggestions: + search_link = f"/search?kw={suggestion}&view=rich" + suggestions_html += f"
" + suggestions_html += f"{suggestion.capitalize()}" + suggestions_html += "
" + suggestions_html += "
" + # Find the last
and insert before it + last_div_pos = response.rfind("
") + if last_div_pos != -1: + response = response[:last_div_pos] + suggestions_html + response[last_div_pos:] else: response = jsonify(response_list) #logging.debug(f"Search Response: {response}") diff --git a/data/concept_mappings.json b/data/concept_mappings.json new file mode 100644 index 0000000..8001fa3 --- /dev/null +++ b/data/concept_mappings.json @@ -0,0 +1,142 @@ +[ + { + "query": "rapture", + "explanation": "The word 'rapture' does not appear in the KJV, but is usually a reference to believers being caught up to meet Christ in the air.", + "search_terms": ["caught up", "meet the lord in the air"] + }, + { + "query": "trinity", + "explanation": "The word 'trinity' does not appear in the KJV, but is usually a reference to the doctrine of the triune God (Father, Son, and Holy Spirit).", + "search_terms": ["father son holy ghost", "three that bear record"] + }, + { + "query": "missionary", + "explanation": "The word 'missionary' does not appear in the KJV, but is usually a reference to being sent to preach the gospel.", + "search_terms": ["go ye therefore", "preach the gospel", "sent"] + }, + { + "query": "bible", + "explanation": "The word 'bible' does not appear in the KJV, but is usually a reference to the scriptures and God's word.", + "search_terms": ["scripture", "word of god", "holy scriptures"] + }, + { + "query": "inerrancy", + "explanation": "The word 'inerrancy' does not appear in the KJV, but is usually a reference to the perfection and preservation of God's word.", + "search_terms": ["word of god", "scripture", "thou hast magnified thy word"] + }, + { + "query": "antichrist system", + "explanation": "The phrase 'antichrist system' is usually a reference to the antichrist and end times deception.", + "search_terms": ["antichrist", "man of sin", "son of perdition", "beast"] + }, + { + "query": "pre-tribulation", + "explanation": "The phrase 'pre-tribulation' does not appear in the KJV, but is usually a reference to Christ's return before the tribulation.", + "search_terms": ["caught up", "great tribulation", "day of the lord"] + }, + { + "query": "soul winning", + "explanation": "The phrase 'soul winning' does not appear in the KJV, but is usually a reference to winning souls and converting sinners.", + "search_terms": ["winneth souls", "save souls", "convert"] + }, + { + "query": "guardian angel", + "explanation": "The phrase 'guardian angel' does not appear in the KJV, but is usually a reference to angels and their protective ministry.", + "search_terms": ["angel", "ministering spirits", "charge over thee"] + }, + { + "query": "angel wings", + "explanation": "The phrase 'angel wings' does not appear in the KJV, but is usually a reference to biblical descriptions of angels.", + "search_terms": ["angel", "seraphim", "cherubim", "wings"] + }, + { + "query": "7 deadly sins", + "explanation": "The phrase '7 deadly sins' does not appear in the KJV (it's a Catholic tradition), but is usually a reference to biblical lists of sins.", + "search_terms": ["abomination", "works of the flesh", "evil"] + }, + { + "query": "purgatory", + "explanation": "The word 'purgatory' does not appear in the KJV. This is a Catholic doctrine not found in scripture.", + "search_terms": ["appointed unto men once to die", "after this the judgment", "absent from the body"] + }, + { + "query": "christmas", + "explanation": "The word 'christmas' does not appear in the KJV, but is usually a reference to the birth of Christ.", + "search_terms": ["born in bethlehem", "manger", "wise men", "shepherds"] + }, + { + "query": "easter", + "explanation": "The word 'easter' appears once in the KJV (Acts 12:4) referring to Passover, but is usually a reference to Christ's resurrection.", + "search_terms": ["easter", "resurrection", "rose again", "third day"] + }, + { + "query": "lucifer", + "explanation": "The name 'Lucifer' appears once in the KJV (Isaiah 14:12), but is usually a reference to Satan and his fall.", + "search_terms": ["lucifer", "devil", "satan", "dragon", "serpent"] + }, + { + "query": "666", + "explanation": "The number '666' is usually a reference to the mark of the beast.", + "search_terms": ["six hundred threescore and six", "mark", "number of his name"] + }, + { + "query": "four horsemen", + "explanation": "The phrase 'four horsemen' does not appear exactly in the KJV, but is usually a reference to the four horsemen of the apocalypse.", + "search_terms": ["horse", "seal", "rider"] + }, + { + "query": "end of the world", + "explanation": "The phrase 'end of the world' is usually a reference to the end times and Christ's return.", + "search_terms": ["end of the world", "last days", "coming of the lord"] + }, + { + "query": "left behind", + "explanation": "The phrase 'left behind' is usually a reference to the rapture and second coming.", + "search_terms": ["one shall be taken", "left", "coming of the son of man"] + }, + { + "query": "prosperity gospel", + "explanation": "The phrase 'prosperity gospel' does not appear in the KJV. This is a false doctrine. Biblical warnings against covetousness and false teachers:", + "search_terms": ["love of money", "covetousness", "false prophets", "perverse disputings"] + }, + { + "query": "speaking in tongues", + "explanation": "The phrase 'speaking in tongues' is usually a reference to tongues and spiritual gifts.", + "search_terms": ["tongues", "unknown tongue", "interpretation"] + }, + { + "query": "slain in the spirit", + "explanation": "The phrase 'slain in the spirit' does not appear in the KJV. This is a charismatic practice not found in scripture.", + "search_terms": ["holy ghost", "filled with the spirit", "spirit of god"] + }, + { + "query": "original sin", + "explanation": "The phrase 'original sin' does not appear in the KJV, but is usually a reference to the doctrine of inherited sin from Adam.", + "search_terms": ["all have sinned", "by one man sin entered", "in adam all die"] + }, + { + "query": "the chosen", + "explanation": "The phrase 'the chosen' is usually a reference to God's elect and chosen people.", + "search_terms": ["chosen", "elect", "called"] + }, + { + "query": "good samaritan", + "explanation": "The phrase 'good samaritan' is usually a reference to the parable of the good Samaritan.", + "search_terms": ["certain samaritan", "neighbour", "priest levite"] + }, + { + "query": "prodigal son", + "explanation": "The phrase 'prodigal son' is usually a reference to the parable of the prodigal son.", + "search_terms": ["younger son", "riotous living", "father said", "was lost"] + }, + { + "query": "jezebel spirit", + "explanation": "The phrase 'jezebel spirit' does not appear in the KJV (it's a charismatic term), but is usually a reference to Jezebel and false prophetesses.", + "search_terms": ["jezebel", "that woman", "seduceth"] + }, + { + "query": "generational curse", + "explanation": "The phrase 'generational curse' does not appear in the KJV (it's a charismatic term), but is usually a reference to visiting iniquity upon children.", + "search_terms": ["visiting the iniquity", "fathers", "children"] + } +] diff --git a/data/grammatical_variants.json b/data/grammatical_variants.json new file mode 100644 index 0000000..069b0c6 --- /dev/null +++ b/data/grammatical_variants.json @@ -0,0 +1,54 @@ +[ + ["you", "ye", "thee", "thou"], + ["your", "thy", "thine"], + ["have", "has", "hath", "hast"], + ["do", "does", "doth", "doest"], + ["say", "says", "said", "saith"], + ["are", "art"], + ["was", "were", "wast", "wert"], + ["will", "wilt", "shall", "shalt"], + ["go", "goes", "goeth", "goest"], + ["know", "knows", "knoweth", "knowest"], + ["see", "sees", "seeth", "seest"], + ["give", "gives", "giveth", "givest"], + ["make", "makes", "maketh", "makest"], + ["come", "comes", "cometh", "comest"], + ["take", "takes", "taketh", "takest"], + ["can", "canst"], + ["may", "mayest"], + ["hear", "hears", "heareth", "hearest"], + ["walk", "walks", "walketh", "walkest"], + ["speak", "speaks", "speaketh", "speakest"], + ["live", "lives", "liveth", "livest"], + ["call", "calls", "calleth", "callest"], + ["fear", "fears", "feareth", "fearest"], + ["love", "loves", "loveth", "lovest"], + ["believe", "believes", "believeth", "believest"], + ["did", "didst"], + ["eat", "eats", "eateth", "eatest"], + ["among", "amongst"], + ["until", "till", "unto"], + ["afterward", "afterwards"], + ["before", "ere"], + ["also", "moreover", "furthermore"], + ["therefore", "wherefore", "thus"], + ["because", "for"], + ["indeed", "verily"], + ["behold", "lo"], + ["ask", "asks", "asketh", "askest"], + ["find", "finds", "findeth", "findest"], + ["seek", "seeks", "seeketh", "seekest"], + ["trust", "trusts", "trusteth", "trustest"], + ["follow", "follows", "followeth", "followest"], + ["serve", "serves", "serveth", "servest"], + ["teach", "teaches", "teacheth", "teachest"], + ["think", "thinks", "thinketh", "thinkest"], + ["lead", "leads", "leadeth", "leadest"], + ["rejoice", "rejoices", "rejoiceth", "rejoicest"], + ["keep", "keeps", "keepeth", "keepest"], + ["abide", "abides", "abideth", "abidest"], + ["judge", "judges", "judgeth", "judgest"], + ["die", "dies", "dieth", "diest"], + ["rise", "rises", "riseth", "risest"], + ["reign", "reigns", "reigneth", "reignest"] +] diff --git a/data/t_kjv.db b/data/t_kjv.db new file mode 100644 index 0000000..e69de29 diff --git a/data/theological_synonyms.json b/data/theological_synonyms.json new file mode 100644 index 0000000..9721899 --- /dev/null +++ b/data/theological_synonyms.json @@ -0,0 +1,104 @@ +[ + ["charity", "love"], + ["ghost", "spirit"], + ["righteousness", "holiness"], + ["salvation", "redemption"], + ["faith", "belief"], + ["sin", "iniquity", "transgression"], + ["forgive", "pardon"], + ["anger", "wrath"], + ["witness", "testify", "testimony"], + ["heaven", "paradise"], + ["hell", "damnation"], + ["pray", "prayer", "supplication"], + ["bless", "blessed", "blessing"], + ["promise", "covenant"], + ["worship", "praise"], + ["disciple", "apostle"], + ["eternal", "everlasting"], + ["demon", "devil"], + ["miracle", "wonder", "sign"], + ["prophesy", "prophecy"], + ["resurrect", "resurrection", "rise", "risen"], + ["baptize", "baptism"], + ["tempt", "temptation"], + ["humble", "meek"], + ["wise", "wisdom"], + ["truth", "true"], + ["glory", "glorify"], + ["mercy", "compassion"], + ["grace", "favor"], + ["judge", "judgment", "condemn", "condemnation"], + ["command", "commandment"], + ["rejoice", "joy", "gladness"], + ["reverence", "fear"], + ["teach", "doctrine"], + ["preach", "proclaim"], + ["suffer", "suffering", "affliction"], + ["comfort", "console"], + ["obey", "obedience"], + ["peace", "rest"], + ["repent", "repentance", "turn"], + ["holy", "sanctify", "sanctified"], + ["nation", "gentile"], + ["Israel", "Jew", "Hebrew"], + ["king", "kingdom", "reign"], + ["priest", "priesthood"], + ["covenant", "testament"], + ["temple", "tabernacle", "sanctuary"], + ["altar", "sacrifice", "offering"], + ["righteous", "just", "justice"], + ["wicked", "evil", "ungodly"], + ["servant", "minister"], + ["fornication", "whoredom", "adultery"], + ["sodomite", "abomination"], + ["pervert", "corrupt"], + ["destroy", "destruction", "perish"], + ["harlot", "whore"], + ["idolater", "idolatry"], + ["covetous", "covetousness", "greed"], + ["enemy", "adversary"], + ["avenge", "vengeance", "recompense"], + ["tribulation", "trouble", "distress"], + ["scorn", "mock", "despise"], + ["proud", "pride", "haughty"], + ["meek", "lowly", "humble"], + ["wisdom", "understanding", "knowledge"], + ["foolish", "fool", "folly"], + ["strengthen", "strong", "might"], + ["weary", "faint"], + ["prevail", "overcome"], + ["remnant", "residue"], + ["inheritance", "heir"], + ["generation", "seed", "offspring"], + ["pestilence", "plague"], + ["famine", "hunger"], + ["captivity", "bondage"], + ["deliver", "deliverance", "save"], + ["redeem", "ransom"], + ["provoke", "anger", "wrath"], + ["abhor", "hate", "detest"], + ["trust", "confidence"], + ["persecute", "persecution"], + ["blaspheme", "blasphemy"], + ["stone", "stoning"], + ["burn", "fire", "flame"], + ["beloved", "wellbeloved"], + ["delight", "pleasure"], + ["kindness", "lovingkindness"], + ["tender", "tenderness"], + ["longsuffering", "patience", "forbearance"], + ["gentleness", "meekness"], + ["goodness", "kindness"], + ["brotherly", "brother"], + ["wrath", "indignation", "fury"], + ["vengeance", "recompense", "reward"], + ["end times", "last days", "latter days"], + ["appearing", "coming", "return"], + ["trumpet", "shout", "voice"], + ["watch", "watchful", "vigilant"], + ["seal", "sealed"], + ["vial", "bowl"], + ["scroll", "book"], + ["lamb", "lion"] +] diff --git a/html/index.html b/html/index.html index 3ff263c..ef78ff6 100644 --- a/html/index.html +++ b/html/index.html @@ -1,77 +1,105 @@ - KJV API - Legacy Edition - + + + + -

KJV API (Legacy edition)

+
+

KJV API (Legacy edition)

-
-
-

- - - +
+ +
+

+ + + + +
-
-
-API ENDPOINT SPECIFICATION v 0.3
+    
+    

Old Testament

+ {% for category, books in testament_data.OT.items() %} +

{{ category }}

+
+ {% for book in books %} +
+ +
+ {% for chapter in range(1, book.chapters + 1) %} + {{ chapter }} + {% endfor %} +
+
+ {% endfor %} +
+ {% endfor %} + + +

New Testament

+ {% for category, books in testament_data.NT.items() %} +

{{ category }}

+
+ {% for book in books %} +
+ +
+ {% for chapter in range(1, book.chapters + 1) %} + {{ chapter }} + {% endfor %} +
+
+ {% endfor %} +
+ {% endfor %} + + + +
+
API ENDPOINT SPECIFICATION v 0.3
 ================================
 
-
-/find - Tries to show a verse, if it's not a verse
+/find - Tries to show a verse, if it's not a verse
        it will try to search your query. If it's a
        single query it will try to find a definition
        for the word as well. Only avaliable in "Rich"
@@ -213,9 +222,9 @@ URL PARAMETERS
 ==============
 view=
 - json - json-encoded response
-- plain - plaintext response
-- rich - pretty response with css
-default: json
+- plain - very simple html response
+- rich - pretty response with css and js
+Required parameter
 
 kw=
 - Used with /search /show/ /define /find
@@ -244,7 +253,7 @@ num=
 FEATURES
 ========
 - King James Only. (No heretical commentaries or false Bibles)
-- High quality source documents 
+- High quality source documents
 - Free
 - Developer hates the sodomites
 
@@ -257,7 +266,6 @@ radio selector for choosing your output format.
 - /define
 - /show
 
-
 EXAMPLES
 ========
 api.tld/random
@@ -266,7 +274,10 @@ api.tld/show?kw=john3:16,1john5:7&view=plain
 
 BUGS
 ====
-Suggestions, bug reports or improvements can be reported to @tyler@1611.social
-
-
+Suggestions, bug reports or improvements can be reported to @tyler@nicecrew.digital
+
+ +
+ + diff --git a/js/collapsible.js b/js/collapsible.js index 4621978..f17bb8e 100644 --- a/js/collapsible.js +++ b/js/collapsible.js @@ -3,12 +3,34 @@ var i; for (i = 0; i < coll.length; i++) { coll[i].addEventListener("click", function() { - this.classList.toggle("active"); var content = this.nextElementSibling; - if (content.style.display === "block") { + var isCurrentlyOpen = content.style.display === "block" || content.style.display === "grid"; + + // If this collapsible has a parent with book-grid class, close all siblings in that grid + var parentGrid = this.closest('.book-grid'); + if (parentGrid) { + 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"; + } + } + } + } + + // Toggle current element + this.classList.toggle("active"); + if (isCurrentlyOpen) { content.style.display = "none"; } else { - content.style.display = "block"; + if (content.classList.contains('chapter-grid')) { + content.style.display = "grid"; + } else { + content.style.display = "block"; + } } }); } diff --git a/static/favicon.svg b/static/favicon.svg new file mode 100644 index 0000000..aed73bb --- /dev/null +++ b/static/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/styles/header.css b/styles/header.css index 3282fa5..57566e5 100644 --- a/styles/header.css +++ b/styles/header.css @@ -89,11 +89,40 @@ body { max-width: var(--content-max-width); margin: 0 auto; padding: var(--space-md); - margin-top: var(--header-height); + margin-top: calc(var(--header-height) + var(--space-md)); color: var(--color-text-primary); white-space: pre-wrap; } +/* Correction & Variant Notices */ +.correction-notice, +.variant-notice, +.theological-notice { + font-family: var(--font-ui); + font-size: 0.95rem; + padding: var(--space-md) var(--space-lg); + margin-bottom: var(--space-lg); + background-color: var(--color-bg-tertiary); + border-left: 4px solid var(--color-accent-primary); + border-radius: var(--border-radius-sm); + color: var(--color-text-secondary); +} + +.correction-notice strong, +.variant-notice strong, +.theological-notice strong { + color: var(--color-accent-primary); + font-weight: 600; +} + +.theological-notice em { + display: block; + margin-top: var(--space-sm); + color: var(--color-text-muted); + font-style: italic; + font-size: 0.9rem; +} + /* Results Count */ .results-count { font-family: var(--font-ui); @@ -103,6 +132,52 @@ body { font-weight: 500; } +/* Theological Suggestions Section */ +.theological-suggestions { + margin-top: var(--space-2xl); + padding: var(--space-lg); + background-color: var(--color-bg-secondary); + border-radius: var(--border-radius); + border: 1px solid var(--color-border); +} + +.theological-suggestions h3 { + font-family: var(--font-ui); + font-size: 1.25rem; + color: var(--color-text-primary); + margin: 0 0 var(--space-md) 0; + font-weight: 600; +} + +.suggestion-item { + font-family: var(--font-ui); + font-size: 1rem; + margin: var(--space-sm) 0; + padding: var(--space-sm) 0; + color: var(--color-text-secondary); +} + +.suggestion-item a { + color: var(--color-accent-primary); + text-decoration: none; + transition: color var(--transition-fast); +} + +.suggestion-item a:hover { + color: var(--color-accent-hover); + text-decoration: underline; +} + +.suggestion-item strong { + font-weight: 600; +} + +.suggestion-item em { + font-style: italic; + color: var(--color-text-muted); + font-size: 0.95rem; +} + /* Dictionary Section */ .dictionary-section { margin: var(--space-xl) 0 var(--space-lg) 0; @@ -212,8 +287,8 @@ button:active { background-color: var(--color-bg-secondary); border-left: 3px solid var(--color-accent-primary); border-radius: var(--border-radius-sm); - padding: 0.75rem 1.25rem; - margin: 0.5rem 0; + padding: 0.5rem 1rem; + margin: 0.15rem 0; transition: background-color var(--transition-fast), border-color var(--transition-fast); line-height: 1; } @@ -249,10 +324,10 @@ button:active { .verse-text { font-family: var(--font-scripture); font-size: 1.15rem; - line-height: 1.8; + line-height: 1.6; color: var(--color-text-primary); text-indent: 0; - margin: 0.35rem 0 0 0; + margin: 0.25rem 0 0 0; padding: 0; display: block; }