Skip to content

Commit c0cfded

Browse files
authored
Change from LunrJs to MinisearchJs for client-side search (#2172)
1 parent 69cde2d commit c0cfded

File tree

10 files changed

+216
-107
lines changed

10 files changed

+216
-107
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
105105

106106
* Documenter now generates a `.documenter-siteinfo.json` file in the HTML build, that contains some metadata about the build. ([#2181])
107107

108+
* The client-side search engine has been changed from LunrJs to MinisearchJs. Additionally, the search results will now contain additional context and have an improved UI. ([#2141])
109+
108110
### Fixed
109111

110112
* Line endings in Markdown source files are now normalized to `LF` before parsing, to work around [a bug in the Julia Markdown parser][julia-29344] where parsing is sensitive to line endings, and can therefore cause platform-dependent behavior. ([#1906])
@@ -1603,6 +1605,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16031605
[#2128]: https://github.com/JuliaDocs/Documenter.jl/issues/2128
16041606
[#2130]: https://github.com/JuliaDocs/Documenter.jl/issues/2130
16051607
[#2134]: https://github.com/JuliaDocs/Documenter.jl/issues/2134
1608+
[#2141]: https://github.com/JuliaDocs/Documenter.jl/issues/2141
16061609
[#2145]: https://github.com/JuliaDocs/Documenter.jl/issues/2145
16071610
[#2153]: https://github.com/JuliaDocs/Documenter.jl/issues/2153
16081611
[#2157]: https://github.com/JuliaDocs/Documenter.jl/issues/2157

assets/html/scss/documenter-dark.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ $input-background-color: $body-background-color;
7070
$input-border-color: $border;
7171
$input-placeholder-color: rgba($input-color, 0.3);
7272

73+
$search-result-link-text-color: #333;
74+
$search-result-link-text-background-color: #f1f5f9;
75+
$search-result-title-text-color: whitesmoke;
76+
7377
$button-static-color: $grey-lighter;
7478
$button-static-background-color: $background;
7579
$button-static-border-color: $border;

assets/html/scss/documenter-light.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ code.language-julia-repl > span.hljs-meta {
4040

4141
// Workaround to compile in highlightjs theme, so that we could have different
4242
// themes for both
43-
@import "highlightjs/default"
43+
@import "highlightjs/default";

assets/html/scss/documenter/_variables.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,13 @@ $documenter-docstring-header-padding: 0.5rem $documenter-container-left-padding;
8989
$documenter-docstring-body-padding-h: $documenter-container-left-padding;
9090
$documenter-docstring-body-padding-v: 0.75rem;
9191
$documenter-docstring-body-padding: $documenter-docstring-body-padding-v $documenter-docstring-body-padding-h;
92+
93+
// Search Results variables
94+
$search-result-link-hover: rgba(0, 128, 128, 0.1) !default;
95+
$search-result-link-text-color: #f1f5f9 !default;
96+
$search-result-link-text-background-color: #333 !default;
97+
$search-result-title-text-color: #333 !default;
98+
$search-result-badge-color: whitesmoke !default;
99+
$search-result-badge-background-color: #33415580 !default;
100+
101+
$search-result-highlight: hsl(48, 100%, 67%) !default;

assets/html/scss/documenter/layout/_search.scss

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,59 @@
1111
li {
1212
margin-left: 2rem;
1313
}
14-
.docs-highlight {
15-
background-color: yellow;
14+
15+
.search-result-link {
16+
border-radius: 0.7em;
17+
transition: all 300ms;
18+
}
19+
20+
.search-result-link:hover, .search-result-link:focus {
21+
background-color: $search-result-link-hover;
22+
}
23+
24+
.search-result-link .property-search-result-badge {
25+
transition: all 300ms;
26+
}
27+
28+
.property-search-result-badge {
29+
padding: 0.15em 0.5em;
30+
font-size: 0.8em;
31+
font-style: italic;
32+
text-transform: none !important;
33+
line-height: 1.5;
34+
color: $search-result-badge-color;
35+
background-color: $search-result-badge-background-color;
36+
border-radius: 0.6rem;
37+
}
38+
39+
.search-result-link:hover .property-search-result-badge, .search-result-link:focus .property-search-result-badge {
40+
color: $search-result-link-text-color;
41+
background-color: $search-result-link-text-background-color;
42+
}
43+
44+
.search-result-highlight {
45+
background-color: $search-result-highlight;
46+
color: black;
47+
}
48+
49+
.search-divider {
50+
border-bottom: 1px solid $border;
51+
}
52+
53+
.search-result-title {
54+
color: $search-result-title-text-color;
55+
}
56+
57+
.w-100 {
58+
width: 100%;
59+
}
60+
61+
.gap-2 {
62+
gap: 0.5rem;
63+
}
64+
65+
.gap-4 {
66+
gap: 1rem;
1667
}
1768
}
1869
}

assets/html/search.js

Lines changed: 141 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
// libraries: jquery, lunr, lodash
2-
// arguments: $, lunr, _
1+
// libraries: jquery, minisearch, lodash
2+
// arguments: $, minisearch, _
33

4-
$(document).ready(function () {
4+
$(function () {
55
// parseUri 1.2.2
66
// (c) Steven Levithan <stevenlevithan.com>
77
// MIT License
@@ -54,10 +54,15 @@ $(document).ready(function () {
5454
e.preventDefault();
5555
});
5656

57+
let ms_data = documenterSearchIndex["docs"].map((x, key) => {
58+
x["id"] = key;
59+
return x;
60+
});
61+
5762
// list below is the lunr 2.1.3 list minus the intersect with names(Base)
5863
// (all, any, get, in, is, only, which) and (do, else, for, let, where, while, with)
5964
// ideally we'd just filter the original list but it's not available as a variable
60-
lunr.stopWordFilter = lunr.generateStopWordFilter([
65+
const stopWords = new Set([
6166
"a",
6267
"able",
6368
"about",
@@ -165,114 +170,150 @@ $(document).ready(function () {
165170
"your",
166171
]);
167172

168-
// add . as a separator, because otherwise "title": "Documenter.Anchors.add!"
169-
// would not find anything if searching for "add!", only for the entire qualification
170-
lunr.tokenizer.separator = /[\s\-\.]+/;
171-
172-
// custom trimmer that doesn't strip @ and !, which are used in julia macro and function names
173-
lunr.trimmer = function (token) {
174-
return token.update(function (s) {
175-
return s.replace(/^[^a-zA-Z0-9@!]+/, "").replace(/[^a-zA-Z0-9@!]+$/, "");
176-
});
177-
};
173+
let index = new minisearch({
174+
fields: ["title", "text"], // fields to index for full-text search
175+
storeFields: ["location", "title", "text", "category", "page"], // fields to return with search results
176+
processTerm: (term) => {
177+
let word = stopWords.has(term) ? null : term;
178+
if (word) {
179+
// custom trimmer that doesn't strip @ and !, which are used in julia macro and function names
180+
word = word
181+
.replace(/^[^a-zA-Z0-9@!]+/, "")
182+
.replace(/[^a-zA-Z0-9@!]+$/, "");
183+
}
178184

179-
lunr.Pipeline.registerFunction(lunr.stopWordFilter, "juliaStopWordFilter");
180-
lunr.Pipeline.registerFunction(lunr.trimmer, "juliaTrimmer");
185+
return word ?? null;
186+
},
187+
// add . as a separator, because otherwise "title": "Documenter.Anchors.add!", would not find anything if searching for "add!", only for the entire qualification
188+
tokenize: (string) => string.split(/[\s\-\.]+/),
189+
searchOptions: {
190+
boost: { title: 100 },
191+
fuzzy: 2,
192+
processTerm: (term) => {
193+
let word = stopWords.has(term) ? null : term;
194+
if (word) {
195+
word = word
196+
.replace(/^[^a-zA-Z0-9@!]+/, "")
197+
.replace(/[^a-zA-Z0-9@!]+$/, "");
198+
}
181199

182-
var index = lunr(function () {
183-
this.ref("location");
184-
this.field("title", { boost: 100 });
185-
this.field("text");
186-
documenterSearchIndex["docs"].forEach(function (e) {
187-
this.add(e);
188-
}, this);
200+
return word ?? null;
201+
},
202+
tokenize: (string) => string.split(/[\s\-\.]+/),
203+
},
189204
});
190-
var store = {};
191205

192-
documenterSearchIndex["docs"].forEach(function (e) {
193-
store[e.location] = { title: e.title, category: e.category, page: e.page };
194-
});
206+
index.addAll(ms_data);
195207

196-
$(function () {
197-
searchresults = $("#documenter-search-results");
198-
searchinfo = $("#documenter-search-info");
199-
searchbox = $("#documenter-search-query");
200-
searchform = $(".docs-search");
201-
sidebar = $(".docs-sidebar");
202-
function update_search(querystring) {
203-
tokens = lunr.tokenizer(querystring);
204-
results = index.query(function (q) {
205-
tokens.forEach(function (t) {
206-
q.term(t.toString(), {
207-
fields: ["title"],
208-
boost: 100,
209-
usePipeline: true,
210-
editDistance: 0,
211-
wildcard: lunr.Query.wildcard.NONE,
212-
});
213-
q.term(t.toString(), {
214-
fields: ["title"],
215-
boost: 10,
216-
usePipeline: true,
217-
editDistance: 2,
218-
wildcard: lunr.Query.wildcard.NONE,
219-
});
220-
q.term(t.toString(), {
221-
fields: ["text"],
222-
boost: 1,
223-
usePipeline: true,
224-
editDistance: 0,
225-
wildcard: lunr.Query.wildcard.NONE,
226-
});
227-
});
228-
});
229-
searchinfo.text("Number of results: " + results.length);
230-
searchresults.empty();
231-
results.forEach(function (result) {
232-
data = store[result.ref];
233-
link = $('<a class="docs-label">' + data.title + "</a>");
234-
link.attr("href", documenterBaseURL + "/" + result.ref);
235-
if (data.category != "page") {
236-
cat = $(
237-
'<span class="docs-category">(' +
238-
data.category +
239-
", " +
240-
data.page +
241-
")</span>"
242-
);
243-
} else {
244-
cat = $('<span class="docs-category">(' + data.category + ")</span>");
245-
}
246-
li = $("<li>").append(link).append(" ").append(cat);
247-
searchresults.append(li);
248-
});
249-
}
208+
searchresults = $("#documenter-search-results");
209+
searchinfo = $("#documenter-search-info");
210+
searchbox = $("#documenter-search-query");
211+
searchform = $(".docs-search");
212+
sidebar = $(".docs-sidebar");
250213

251-
function update_search_box() {
252-
querystring = searchbox.val();
253-
update_search(querystring);
254-
}
214+
function update_search(querystring) {
215+
let results = [];
216+
results = index.search(querystring, {
217+
filter: (result) => result.score >= 1,
218+
});
255219

256-
searchbox.keyup(_.debounce(update_search_box, 250));
257-
searchbox.change(update_search_box);
220+
searchresults.empty();
258221

259-
// Disable enter-key form submission for the searchbox on the search page
260-
// and just re-run search rather than refresh the whole page.
261-
searchform.keypress(function (event) {
262-
if (event.which == "13") {
263-
if (sidebar.hasClass("visible")) {
264-
sidebar.removeClass("visible");
222+
let links = [];
223+
let count = 0;
224+
225+
results.forEach(function (result) {
226+
if (result.location) {
227+
if (!links.includes(result.location)) {
228+
searchresults.append(make_search_result(result, querystring));
229+
count++;
265230
}
266-
update_search_box();
267-
event.preventDefault();
231+
232+
links.push(result.location);
268233
}
269234
});
270235

271-
search_query_uri = parseUri(window.location).queryKey["q"];
272-
if (search_query_uri !== undefined) {
273-
search_query = decodeURIComponent(search_query_uri.replace(/\+/g, "%20"));
274-
searchbox.val(search_query);
236+
searchinfo.text("Number of results: " + count);
237+
}
238+
239+
function make_search_result(result, querystring) {
240+
let display_link =
241+
result.location.slice(Math.max(0), Math.min(50, result.location.length)) +
242+
(result.location.length > 30 ? "..." : "");
243+
244+
let textindex = new RegExp(`\\b${querystring}\\b`, "i").exec(result.text);
245+
let text =
246+
textindex !== null
247+
? result.text.slice(
248+
Math.max(textindex.index - 100, 0),
249+
Math.min(
250+
textindex.index + querystring.length + 100,
251+
result.text.length
252+
)
253+
)
254+
: "";
255+
256+
let display_result = text.length
257+
? "..." +
258+
text.replace(
259+
new RegExp(`\\b${querystring}\\b`, "i"), // For first occurrence
260+
'<span class="search-result-highlight p-1">$&</span>'
261+
) +
262+
"..."
263+
: "";
264+
265+
let result_div = `
266+
<a href="${
267+
documenterBaseURL + "/" + result.location
268+
}" class="search-result-link px-4 py-2 w-100 is-flex is-flex-direction-column gap-2 my-4">
269+
<div class="w-100 is-flex is-flex-wrap-wrap is-justify-content-space-between is-align-items-center">
270+
<div class="search-result-title has-text-weight-semi-bold">${
271+
result.title
272+
}</div>
273+
<div class="property-search-result-badge">${result.category}</div>
274+
</div>
275+
<p>
276+
${display_result}
277+
</p>
278+
<div
279+
class="has-text-left"
280+
style="font-size: smaller;"
281+
title="${result.location}"
282+
>
283+
<i class="fas fa-link"></i> ${display_link}
284+
</div>
285+
</a>
286+
<div class="search-divider"></div>
287+
`;
288+
return result_div;
289+
}
290+
291+
function update_search_box() {
292+
querystring = searchbox.val();
293+
update_search(querystring);
294+
}
295+
296+
searchbox.keyup(_.debounce(update_search_box, 250));
297+
searchbox.change(update_search_box);
298+
299+
// Disable enter-key form submission for the searchbox on the search page
300+
// and just re-run search rather than refresh the whole page.
301+
searchform.keypress(function (event) {
302+
if (event.which == "13") {
303+
if (sidebar.hasClass("visible")) {
304+
sidebar.removeClass("visible");
305+
}
306+
update_search_box();
307+
event.preventDefault();
275308
}
276-
update_search_box();
277309
});
310+
311+
search_query_uri = parseUri(window.location).queryKey["q"];
312+
313+
if (search_query_uri !== undefined) {
314+
search_query = decodeURIComponent(search_query_uri.replace(/\+/g, "%20"));
315+
searchbox.val(search_query);
316+
}
317+
318+
update_search_box();
278319
});

assets/html/themes/documenter-dark.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/html/themes/documenter-light.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/html/HTMLWriter.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ function render(doc::Documenter.Document, settings::HTML=HTML())
686686
if isfile(joinpath(doc.user.source, "assets", "search.js"))
687687
@warn "not creating 'search.js', provided by the user."
688688
else
689-
r = JSDependencies.RequireJS([RD.jquery, RD.lunr, RD.lodash])
689+
r = JSDependencies.RequireJS([RD.jquery, RD.minisearch, RD.lodash])
690690
push!(r, JSDependencies.parse_snippet(joinpath(ASSETS, "search.js")))
691691
JSDependencies.verify(r; verbose=true) || error("RequireJS declaration is invalid")
692692
JSDependencies.writejs(joinpath(doc.user.build, "assets", "search.js"), r)

0 commit comments

Comments
 (0)