Public Access
Site / build (pull_request) Successful in 12s
A search page whose index is the page itself: one item for each page and each heading of the guide, the how-tos, the FAQ and the developer docs, written by Zola from the pages' own content. A small script filters and ranks them as you type. Nothing is fetched, so the Content-Security-Policy needs nothing new; without JavaScript the page is a list of every heading. The navigation gets a link, and the documentation's index pages a box that is a plain form to /search/?q=. The devlog is not searched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
121 lines
5.6 KiB
JavaScript
121 lines
5.6 KiB
JavaScript
// 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();
|
|
});
|
|
})();
|