"+notice, 1)
}
// fallbackResp shows the results of a rewritten query with an explanation.
func (a *App) fallbackResp(results []Verse, view, kw, plainNote, richNotice string, jsonObj map[string]any) Resp {
r := a.render(results, view, kw)
switch view {
case "plain":
return htmlR(plainNote + "\n
" + r.text)
case "rich":
return htmlR(insertAfterResponsesOpen(r.text, richNotice))
}
jsonObj["results"] = r.list
return jsonR(jsonObj)
}
func (a *App) handleSearch(kw, view string) Resp {
cleaned := searchClean(kw)
if words := strings.Fields(cleaned); len(words) > maxSearchWords || len(cleaned) > maxSearchLen {
return htmlR("Your query is too long. 20 words or less.")
}
if cleaned == "" {
return a.staticPage("html/search.html")
}
esc := html.EscapeString
// a common theological concept that is not in the KJV?
conceptNotice := ""
if c, ok := a.concepts[strings.ToLower(cleaned)]; ok {
var links []string
for _, t := range c.SearchTerms {
links = append(links, fmt.Sprintf("
%s", strings.ReplaceAll(t, " ", "+"), t))
}
conceptNotice = fmt.Sprintf("
%s: %s
You may find what you are looking for with these search terms: %s
",
capitalize(cleaned), c.Explanation, strings.Join(links, ", "))
cleaned = c.SearchTerms[0]
}
results := a.fts(cleaned)
words := strings.Fields(cleaned)
if len(results) > 0 && len(words) > 1 {
scores := make(map[int64]float64, len(results))
for _, v := range results {
scores[v.ID] = proximityScore(v.Text, words)
}
sort.SliceStable(results, func(i, j int) bool { return scores[results[i].ID] > scores[results[j].ID] })
}
if len(results) == 0 {
// 1. grammatical variants
for _, word := range words {
for _, variant := range sortVariants(word, a.variants[strings.ToLower(word)]) {
vq := strings.Replace(cleaned, word, variant, 1)
if r := a.fts(vq); len(r) > 0 {
return a.fallbackResp(r, view, kw,
fmt.Sprintf("Showing results for '%s' (grammatical variant of '%s')", vq, cleaned),
fmt.Sprintf("
Showing results for '%s' (grammatical variant of '%s')
", vq, cleaned),
map[string]any{"original": cleaned, "variant": vq})
}
}
}
// 2. spell check
corrected := make([]string, len(words))
changed := false
for i, w := range words {
corrected[i] = w
if !a.speller.Spell(w) {
if sug := a.speller.Suggest(w); len(sug) > 0 {
corrected[i], changed = sug[0], true
}
}
}
if changed {
ck := strings.Join(corrected, " ")
if r := a.fts(ck); len(r) > 0 {
return a.fallbackResp(r, view, kw,
fmt.Sprintf("Showing results for '%s' (corrected from '%s')", ck, cleaned),
fmt.Sprintf("
Showing results for '%s' (corrected from '%s')
", esc(ck), cleaned),
map[string]any{"corrected_from": cleaned, "corrected_to": ck})
}
}
// 3. theological equivalents
for _, word := range words {
for _, syn := range a.synonyms[strings.ToLower(word)] {
sq := strings.Replace(cleaned, word, syn, 1)
if r := a.fts(sq); len(r) > 0 {
explanation := fmt.Sprintf("In the KJV, '%s' and '%s' often refer to similar theological concepts. Consider also searching for related verses.", word, syn)
return a.fallbackResp(r, view, kw,
fmt.Sprintf("Showing results for '%s' (theological equivalent of '%s')\n%s", sq, cleaned, explanation),
fmt.Sprintf("
Showing results for '%s' (theological equivalent of '%s')
%s
", sq, cleaned, explanation),
map[string]any{"original": cleaned, "theological_equivalent": sq, "explanation": explanation})
}
}
}
// 4. nothing: offer spelling suggestions
var suggestions []string
for i, w := range words {
if a.speller.Spell(w) {
continue
}
for _, s := range a.speller.Suggest(w) {
if len(suggestions) >= 5 {
break
}
alt := append([]string(nil), words...)
alt[i] = s
suggestions = append(suggestions, strings.Join(alt, " "))
}
}
if view != "rich" {
list := "No suggestions available"
if len(suggestions) > 0 {
list = strings.Join(suggestions, ", ")
}
return htmlR(fmt.Sprintf("%s not found, try one of the following: %s", cleaned, list))
}
var sb strings.Builder
sb.WriteString(a.renderNavbar(cleaned, nil, ""))
sb.WriteString("
")
fmt.Fprintf(&sb, "
No results found for '%s'.
", cleaned)
if len(suggestions) > 0 {
sb.WriteString("
Did you mean?
")
sb.WriteString("
We couldn't find any verses matching your search. Here are some spelling suggestions:
")
for _, s := range suggestions {
fmt.Fprintf(&sb, "
", url.QueryEscape(s), esc(s))
}
sb.WriteString("
")
} else {
sb.WriteString("
Search Tips
")
sb.WriteString("
We couldn't find any verses matching your search. Try:
")
sb.WriteString("
")
sb.WriteString("- Checking your spelling
- Using different keywords
")
sb.WriteString("- Searching for love, faith, or hope
")
sb.WriteString("
")
}
sb.WriteString("
")
return htmlR(sb.String())
}
r := a.render(results, view, kw)
var related []string
for _, w := range words {
related = append(related, a.synonyms[strings.ToLower(w)]...)
}
switch view {
case "plain":
out := r.text
if conceptNotice != "" {
out = conceptNotice + "
" + out
}
if len(related) > 0 {
out += "
Also consider searching: "
for _, s := range related {
out += s + "
"
}
}
return htmlR(out)
case "rich":
out := r.text
if conceptNotice != "" {
if start := strings.Index(out, "
"); start != -1 {
if end := strings.Index(out[start+len("
"):], "
"); end != -1 {
at := start + len("
") + end + len("
")
out = out[:at] + conceptNotice + out[at:]
}
}
}
if len(related) > 0 {
var sb strings.Builder
sb.WriteString("
Related Theological Concepts
")
sb.WriteString("
In the KJV, these terms often refer to similar theological concepts. Consider also searching for related verses.
")
for _, s := range related {
fmt.Fprintf(&sb, "
", url.QueryEscape(s), capitalize(s))
}
sb.WriteString("
")
if at := strings.LastIndex(out, "
"); at != -1 {
out = out[:at] + sb.String() + out[at:]
}
}
return htmlR(out)
}
return r.resp()
}
// ---------------------------------------------------------------------------
// /find : scripture if it reads as scripture, otherwise a keyword search with definitions
func (a *App) handleFind(q url.Values) Resp {
query := strings.TrimSpace(q.Get("kw"))
if viewOf(q, "rich") != "rich" {
return jsonR(map[string]string{"Error": "/find endpoint only supports rich responses"})
}
lg.Info("Query: %s", query)
if query == "" {
return a.staticPage("html/find.html")
}
query = strings.TrimSuffix(query, ",")
// A query that reads as scripture references ("Exodus", "1 jn 3.16") is shown
// as scripture; anything else is a keyword search.
if resolved := a.res.Resolve(query, false); resolved != nil {
if resp, ok := a.show(query, "rich", false); ok {
notices := ""
for _, n := range resolved.Notes {
notices += fmt.Sprintf("
Showing %s (interpreted from '%s')
",
html.EscapeString(n.Name), html.EscapeString(n.Original))
}
if resolved.Bare {
keyword := url.QueryEscape(searchClean(query))
notices += fmt.Sprintf("
",
html.EscapeString(resolved.Label), keyword, html.EscapeString(query))
}
if notices != "" {
resp.Body = insertAfterResponsesOpen(resp.Body, notices)
}
return resp
}
}
if strings.TrimSpace(searchClean(query)) == "" {
return a.staticPage("html/find.html") // nothing searchable left (punctuation, accents)
}
var tokens []string
for _, w := range strings.Split(strings.ReplaceAll(query, ",", " "), " ") {
if w != "" {
tokens = append(tokens, w)
}
}
if len(tokens) > 20 {
return htmlR("Your Query is too long. 20 Words or less.")
}
result := a.handleSearch(q.Get("kw"), "rich")
body := strings.TrimSuffix(result.Body, "
") // reopen .responses to add the dictionary inside it
var defs strings.Builder
for _, word := range tokens {
entry, ok := a.dictionary[strings.ToLower(word)]
if !ok {
continue
}
defs.WriteString("