| 1 | // Package query reads the shape of a search box, not its meaning. |
| 2 | // |
| 3 | // It exists so that one decision — is this person looking something up, |
| 4 | // or asking about the corpus? — has a single home, is unit-testable |
| 5 | // without a browser, and is made on the server. The browser script that |
| 6 | // acts on it only honours a flag the server set; it does not re-derive |
| 7 | // the rule, because two copies of a heuristic drift and the drift is |
| 8 | // silent. |
| 9 | package query |
| 10 | |
| 11 | import ( |
| 12 | "strings" |
| 13 | "unicode" |
| 14 | ) |
| 15 | |
| 16 | // openers are the words that, first in a query, mean the person is |
| 17 | // asking rather than looking up. |
| 18 | // |
| 19 | // Interrogatives and auxiliaries only. Each one costs a false positive |
| 20 | // when it is also an ordinary search term ("will" the noun, "can" the |
| 21 | // verb), and that trade is deliberately one-sided: a missed prime makes |
| 22 | // someone press Enter twice, while a wrong prime navigates away from a |
| 23 | // page they were still reading. The safe direction is not priming. |
| 24 | var openers = map[string]bool{ |
| 25 | "who": true, "whom": true, "whose": true, "what": true, "when": true, |
| 26 | "where": true, "which": true, "why": true, "how": true, |
| 27 | |
| 28 | "am": true, "is": true, "are": true, "was": true, "were": true, |
| 29 | "do": true, "does": true, "did": true, |
| 30 | "can": true, "could": true, "should": true, "would": true, "will": true, |
| 31 | "has": true, "have": true, "had": true, "may": true, "might": true, |
| 32 | "must": true, |
| 33 | } |
| 34 | |
| 35 | // PrimesFirstResult reports whether the first of results results should |
| 36 | // be the one Enter goes to. |
| 37 | // |
| 38 | // Both terms of the decision live here rather than at the call site, |
| 39 | // because only one of them is visible from outside. An HTTP test cannot |
| 40 | // see the results > 0 term at all: with no results the template renders |
| 41 | // no list items, so priming nothing and priming a result that does not |
| 42 | // exist produce byte-identical markup. A guard whose failure is |
| 43 | // invisible is a guard written to pass, so this is the level the guard |
| 44 | // is tested at. |
| 45 | func PrimesFirstResult(q string, results int) bool { |
| 46 | return results > 0 && !IsQuestion(q) |
| 47 | } |
| 48 | |
| 49 | // IsQuestion reports whether a query reads as a question about the |
| 50 | // corpus rather than a lookup in it. |
| 51 | // |
| 52 | // Two signals, both cheap and both explicable to the person typing: it |
| 53 | // ends in a question mark, or it opens with an interrogative or an |
| 54 | // auxiliary. Nothing about length — "notes from the March offsite" is |
| 55 | // six words and still a lookup, and a rule that punished it would make |
| 56 | // Enter unreliable in exactly the case it exists to serve. |
| 57 | func IsQuestion(q string) bool { |
| 58 | q = strings.TrimSpace(q) |
| 59 | if q == "" { |
| 60 | return false |
| 61 | } |
| 62 | if strings.HasSuffix(q, "?") { |
| 63 | return true |
| 64 | } |
| 65 | return openers[firstWord(q)] |
| 66 | } |
| 67 | |
| 68 | // firstWord is the leading run of letters, lowercased. |
| 69 | // |
| 70 | // Letters only, and only the leading run, so that "who's" opens with |
| 71 | // "who" while "whatever" and "canary" open with themselves — a prefix |
| 72 | // match here would have made every query beginning "can…" or "will…" |
| 73 | // unprimeable. |
| 74 | func firstWord(q string) string { |
| 75 | var b strings.Builder |
| 76 | for _, r := range q { |
| 77 | if !unicode.IsLetter(r) { |
| 78 | break |
| 79 | } |
| 80 | b.WriteRune(unicode.ToLower(r)) |
| 81 | } |
| 82 | return b.String() |
| 83 | } |
| 84 | |