diff --git a/app.py b/app.py index 0a55bbb..83a2a0e 100644 --- a/app.py +++ b/app.py @@ -113,6 +113,10 @@ for group in theological_synonym_groups: # 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": [ @@ -123,6 +127,7 @@ cached: dict = { "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(), @@ -470,34 +475,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 @@ -663,7 +689,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: @@ -789,7 +815,88 @@ 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() @@ -904,9 +1011,45 @@ def search(): # 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) if suggestions else "No suggestions available" - ) + 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 += "
" + 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) # Check if any words in the search have theological synonyms to suggest @@ -918,15 +1061,27 @@ def search(): for synonym in synonyms: theological_suggestions.append(synonym) - arg_view = request.args.get("view", None) + 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

" 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 index f171d69..069b0c6 100644 --- a/data/grammatical_variants.json +++ b/data/grammatical_variants.json @@ -25,5 +25,30 @@ ["love", "loves", "loveth", "lovest"], ["believe", "believes", "believeth", "believest"], ["did", "didst"], - ["eat", "eats", "eateth", "eatest"] + ["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 index bc077a0..9721899 100644 --- a/data/theological_synonyms.json +++ b/data/theological_synonyms.json @@ -3,5 +3,102 @@ ["ghost", "spirit"], ["righteousness", "holiness"], ["salvation", "redemption"], - ["faith", "belief"] + ["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/styles/header.css b/styles/header.css index 5cee892..57566e5 100644 --- a/styles/header.css +++ b/styles/header.css @@ -287,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; } @@ -324,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; }