// The search page (issue #60). The index is the page itself: one list item for each page of the // documentation and each of its headings, with that part's text. This filters and ranks them. // Nothing is fetched, and nothing typed here leaves the browser. (function () { // Where a match counts for more: what a user came for before what a developer wrote down. var weight = { "Guide": 1.4, "How-to": 1.3, "FAQ": 1.3, "Decisions": 0.8, "Milestones": 0.5 }; var kMax = 40, kAround = 90; document.addEventListener("DOMContentLoaded", function () { var input = document.getElementById("q"), results = document.getElementById("s-results"); var status = document.getElementById("s-status"), index = document.getElementById("s-index"); if (!input || !results || !index) return; var entries = Array.prototype.map.call(index.querySelectorAll(".s-entry"), function (li) { var link = li.querySelector("a"), where = li.querySelector(".s-where"), text = li.querySelector(".s-text"); var body = text ? text.textContent.replace(/\s+/g, " ").trim() : ""; return { href: link.getAttribute("href"), title: link.textContent, where: where ? where.textContent : "", body: body, titleLow: link.textContent.toLowerCase(), whereLow: (where ? where.textContent : "").toLowerCase(), bodyLow: body.toLowerCase(), weight: weight[li.getAttribute("data-group")] || 1 }; }); function count(hay, word) { var n = 0, at = hay.indexOf(word); while (at >= 0 && n < 5) { n++; at = hay.indexOf(word, at + word.length); } return n; } function search(words) { var hits = [], phrase = words.join(" "); entries.forEach(function (e) { // The words as typed, side by side, count for more than the same words scattered. var score = words.length > 1 ? (e.titleLow.indexOf(phrase) >= 0 ? 30 : 0) + (e.bodyLow.indexOf(phrase) >= 0 ? 12 : 0) : 0; if (e.titleLow === phrase) score += 20; for (var i = 0; i < words.length; i++) { var w = words[i], inTitle = e.titleLow.indexOf(w) >= 0, inWhere = e.whereLow.indexOf(w) >= 0, n = count(e.bodyLow, w); if (!inTitle && !inWhere && !n) return; // every word has to be there score += (inTitle ? 20 : 0) + (inWhere ? 3 : 0) + n; } hits.push({ entry: e, score: score * e.weight }); }); hits.sort(function (a, b) { return b.score - a.score; }); return hits; } // The text around the first word found, with every word marked. function snippet(e, words) { var p = document.createElement("p"), first = -1; words.forEach(function (w) { var at = e.bodyLow.indexOf(w); if (at >= 0 && (first < 0 || at < first)) first = at; }); var from = Math.max(0, (first < 0 ? 0 : first) - kAround), to = Math.min(e.body.length, from + 2 * kAround + 40); if (from > 0) { var space = e.body.indexOf(" ", from); if (space >= 0 && space < from + 20) from = space + 1; } var piece = e.body.slice(from, to), low = piece.toLowerCase(), at = 0; if (from > 0) p.appendChild(document.createTextNode("… ")); while (at < piece.length) { var next = -1, len = 0; words.forEach(function (w) { var i = low.indexOf(w, at); if (i >= 0 && (next < 0 || i < next)) { next = i; len = w.length; } }); if (next < 0) { p.appendChild(document.createTextNode(piece.slice(at))); break; } if (next > at) p.appendChild(document.createTextNode(piece.slice(at, next))); var mark = document.createElement("mark"); mark.textContent = piece.slice(next, next + len); p.appendChild(mark); at = next + len; } if (to < e.body.length) p.appendChild(document.createTextNode(" …")); return p; } function show() { var q = input.value.trim(), words = q.toLowerCase().split(/\s+/).filter(function (w) { return w.length > 0; }); while (results.firstChild) results.removeChild(results.firstChild); try { history.replaceState(null, "", q ? "?q=" + encodeURIComponent(q) : location.pathname); } catch (e) { /* a file: page */ } if (!words.length) { results.hidden = true; index.hidden = false; status.textContent = ""; return; } var hits = search(words); hits.slice(0, kMax).forEach(function (h) { var li = document.createElement("li"), a = document.createElement("a"), where = document.createElement("span"); a.href = h.entry.href; a.className = "accent-link"; a.textContent = h.entry.title; where.className = "s-where"; where.textContent = h.entry.where; li.appendChild(a); li.appendChild(where); li.appendChild(snippet(h.entry, words)); results.appendChild(li); }); results.hidden = false; index.hidden = true; status.textContent = !hits.length ? "Nothing found for “" + q + "”. Every word has to be on the page." : (hits.length > kMax ? "The first " + kMax + " of " + hits.length : hits.length === 1 ? "1 place" : hits.length + " places") + " for “" + q + "”."; } var timer = 0; input.addEventListener("input", function () { clearTimeout(timer); timer = setTimeout(show, 80); }); input.form.addEventListener("submit", function (e) { e.preventDefault(); show(); }); var asked = /[?&]q=([^&]*)/.exec(location.search); if (asked) { try { input.value = decodeURIComponent(asked[1].replace(/\+/g, " ")); } catch (e) { /* a bad escape: an empty box */ } } show(); input.focus(); }); })();