Skip to content

Commit

Permalink
Change from LunrJs to MinisearchJs for client-side search (#2172)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hetarth02 authored Aug 4, 2023
1 parent 69cde2d commit c0cfded
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 107 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

* 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])

### Fixed

* 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])
Expand Down Expand Up @@ -1603,6 +1605,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#2128]: https://github.com/JuliaDocs/Documenter.jl/issues/2128
[#2130]: https://github.com/JuliaDocs/Documenter.jl/issues/2130
[#2134]: https://github.com/JuliaDocs/Documenter.jl/issues/2134
[#2141]: https://github.com/JuliaDocs/Documenter.jl/issues/2141
[#2145]: https://github.com/JuliaDocs/Documenter.jl/issues/2145
[#2153]: https://github.com/JuliaDocs/Documenter.jl/issues/2153
[#2157]: https://github.com/JuliaDocs/Documenter.jl/issues/2157
Expand Down
4 changes: 4 additions & 0 deletions assets/html/scss/documenter-dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ $input-background-color: $body-background-color;
$input-border-color: $border;
$input-placeholder-color: rgba($input-color, 0.3);

$search-result-link-text-color: #333;
$search-result-link-text-background-color: #f1f5f9;
$search-result-title-text-color: whitesmoke;

$button-static-color: $grey-lighter;
$button-static-background-color: $background;
$button-static-border-color: $border;
Expand Down
2 changes: 1 addition & 1 deletion assets/html/scss/documenter-light.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ code.language-julia-repl > span.hljs-meta {

// Workaround to compile in highlightjs theme, so that we could have different
// themes for both
@import "highlightjs/default"
@import "highlightjs/default";
10 changes: 10 additions & 0 deletions assets/html/scss/documenter/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,13 @@ $documenter-docstring-header-padding: 0.5rem $documenter-container-left-padding;
$documenter-docstring-body-padding-h: $documenter-container-left-padding;
$documenter-docstring-body-padding-v: 0.75rem;
$documenter-docstring-body-padding: $documenter-docstring-body-padding-v $documenter-docstring-body-padding-h;

// Search Results variables
$search-result-link-hover: rgba(0, 128, 128, 0.1) !default;
$search-result-link-text-color: #f1f5f9 !default;
$search-result-link-text-background-color: #333 !default;
$search-result-title-text-color: #333 !default;
$search-result-badge-color: whitesmoke !default;
$search-result-badge-background-color: #33415580 !default;

$search-result-highlight: hsl(48, 100%, 67%) !default;
55 changes: 53 additions & 2 deletions assets/html/scss/documenter/layout/_search.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,59 @@
li {
margin-left: 2rem;
}
.docs-highlight {
background-color: yellow;

.search-result-link {
border-radius: 0.7em;
transition: all 300ms;
}

.search-result-link:hover, .search-result-link:focus {
background-color: $search-result-link-hover;
}

.search-result-link .property-search-result-badge {
transition: all 300ms;
}

.property-search-result-badge {
padding: 0.15em 0.5em;
font-size: 0.8em;
font-style: italic;
text-transform: none !important;
line-height: 1.5;
color: $search-result-badge-color;
background-color: $search-result-badge-background-color;
border-radius: 0.6rem;
}

.search-result-link:hover .property-search-result-badge, .search-result-link:focus .property-search-result-badge {
color: $search-result-link-text-color;
background-color: $search-result-link-text-background-color;
}

.search-result-highlight {
background-color: $search-result-highlight;
color: black;
}

.search-divider {
border-bottom: 1px solid $border;
}

.search-result-title {
color: $search-result-title-text-color;
}

.w-100 {
width: 100%;
}

.gap-2 {
gap: 0.5rem;
}

.gap-4 {
gap: 1rem;
}
}
}
241 changes: 141 additions & 100 deletions assets/html/search.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// libraries: jquery, lunr, lodash
// arguments: $, lunr, _
// libraries: jquery, minisearch, lodash
// arguments: $, minisearch, _

$(document).ready(function () {
$(function () {
// parseUri 1.2.2
// (c) Steven Levithan <stevenlevithan.com>
// MIT License
Expand Down Expand Up @@ -54,10 +54,15 @@ $(document).ready(function () {
e.preventDefault();
});

let ms_data = documenterSearchIndex["docs"].map((x, key) => {
x["id"] = key;
return x;
});

// list below is the lunr 2.1.3 list minus the intersect with names(Base)
// (all, any, get, in, is, only, which) and (do, else, for, let, where, while, with)
// ideally we'd just filter the original list but it's not available as a variable
lunr.stopWordFilter = lunr.generateStopWordFilter([
const stopWords = new Set([
"a",
"able",
"about",
Expand Down Expand Up @@ -165,114 +170,150 @@ $(document).ready(function () {
"your",
]);

// add . as a separator, because otherwise "title": "Documenter.Anchors.add!"
// would not find anything if searching for "add!", only for the entire qualification
lunr.tokenizer.separator = /[\s\-\.]+/;

// custom trimmer that doesn't strip @ and !, which are used in julia macro and function names
lunr.trimmer = function (token) {
return token.update(function (s) {
return s.replace(/^[^a-zA-Z0-9@!]+/, "").replace(/[^a-zA-Z0-9@!]+$/, "");
});
};
let index = new minisearch({
fields: ["title", "text"], // fields to index for full-text search
storeFields: ["location", "title", "text", "category", "page"], // fields to return with search results
processTerm: (term) => {
let word = stopWords.has(term) ? null : term;
if (word) {
// custom trimmer that doesn't strip @ and !, which are used in julia macro and function names
word = word
.replace(/^[^a-zA-Z0-9@!]+/, "")
.replace(/[^a-zA-Z0-9@!]+$/, "");
}

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

var index = lunr(function () {
this.ref("location");
this.field("title", { boost: 100 });
this.field("text");
documenterSearchIndex["docs"].forEach(function (e) {
this.add(e);
}, this);
return word ?? null;
},
tokenize: (string) => string.split(/[\s\-\.]+/),
},
});
var store = {};

documenterSearchIndex["docs"].forEach(function (e) {
store[e.location] = { title: e.title, category: e.category, page: e.page };
});
index.addAll(ms_data);

$(function () {
searchresults = $("#documenter-search-results");
searchinfo = $("#documenter-search-info");
searchbox = $("#documenter-search-query");
searchform = $(".docs-search");
sidebar = $(".docs-sidebar");
function update_search(querystring) {
tokens = lunr.tokenizer(querystring);
results = index.query(function (q) {
tokens.forEach(function (t) {
q.term(t.toString(), {
fields: ["title"],
boost: 100,
usePipeline: true,
editDistance: 0,
wildcard: lunr.Query.wildcard.NONE,
});
q.term(t.toString(), {
fields: ["title"],
boost: 10,
usePipeline: true,
editDistance: 2,
wildcard: lunr.Query.wildcard.NONE,
});
q.term(t.toString(), {
fields: ["text"],
boost: 1,
usePipeline: true,
editDistance: 0,
wildcard: lunr.Query.wildcard.NONE,
});
});
});
searchinfo.text("Number of results: " + results.length);
searchresults.empty();
results.forEach(function (result) {
data = store[result.ref];
link = $('<a class="docs-label">' + data.title + "</a>");
link.attr("href", documenterBaseURL + "/" + result.ref);
if (data.category != "page") {
cat = $(
'<span class="docs-category">(' +
data.category +
", " +
data.page +
")</span>"
);
} else {
cat = $('<span class="docs-category">(' + data.category + ")</span>");
}
li = $("<li>").append(link).append(" ").append(cat);
searchresults.append(li);
});
}
searchresults = $("#documenter-search-results");
searchinfo = $("#documenter-search-info");
searchbox = $("#documenter-search-query");
searchform = $(".docs-search");
sidebar = $(".docs-sidebar");

function update_search_box() {
querystring = searchbox.val();
update_search(querystring);
}
function update_search(querystring) {
let results = [];
results = index.search(querystring, {
filter: (result) => result.score >= 1,
});

searchbox.keyup(_.debounce(update_search_box, 250));
searchbox.change(update_search_box);
searchresults.empty();

// Disable enter-key form submission for the searchbox on the search page
// and just re-run search rather than refresh the whole page.
searchform.keypress(function (event) {
if (event.which == "13") {
if (sidebar.hasClass("visible")) {
sidebar.removeClass("visible");
let links = [];
let count = 0;

results.forEach(function (result) {
if (result.location) {
if (!links.includes(result.location)) {
searchresults.append(make_search_result(result, querystring));
count++;
}
update_search_box();
event.preventDefault();

links.push(result.location);
}
});

search_query_uri = parseUri(window.location).queryKey["q"];
if (search_query_uri !== undefined) {
search_query = decodeURIComponent(search_query_uri.replace(/\+/g, "%20"));
searchbox.val(search_query);
searchinfo.text("Number of results: " + count);
}

function make_search_result(result, querystring) {
let display_link =
result.location.slice(Math.max(0), Math.min(50, result.location.length)) +
(result.location.length > 30 ? "..." : "");

let textindex = new RegExp(`\\b${querystring}\\b`, "i").exec(result.text);
let text =
textindex !== null
? result.text.slice(
Math.max(textindex.index - 100, 0),
Math.min(
textindex.index + querystring.length + 100,
result.text.length
)
)
: "";

let display_result = text.length
? "..." +
text.replace(
new RegExp(`\\b${querystring}\\b`, "i"), // For first occurrence
'<span class="search-result-highlight p-1">$&</span>'
) +
"..."
: "";

let result_div = `
<a href="${
documenterBaseURL + "/" + result.location
}" class="search-result-link px-4 py-2 w-100 is-flex is-flex-direction-column gap-2 my-4">
<div class="w-100 is-flex is-flex-wrap-wrap is-justify-content-space-between is-align-items-center">
<div class="search-result-title has-text-weight-semi-bold">${
result.title
}</div>
<div class="property-search-result-badge">${result.category}</div>
</div>
<p>
${display_result}
</p>
<div
class="has-text-left"
style="font-size: smaller;"
title="${result.location}"
>
<i class="fas fa-link"></i> ${display_link}
</div>
</a>
<div class="search-divider"></div>
`;
return result_div;
}

function update_search_box() {
querystring = searchbox.val();
update_search(querystring);
}

searchbox.keyup(_.debounce(update_search_box, 250));
searchbox.change(update_search_box);

// Disable enter-key form submission for the searchbox on the search page
// and just re-run search rather than refresh the whole page.
searchform.keypress(function (event) {
if (event.which == "13") {
if (sidebar.hasClass("visible")) {
sidebar.removeClass("visible");
}
update_search_box();
event.preventDefault();
}
update_search_box();
});

search_query_uri = parseUri(window.location).queryKey["q"];

if (search_query_uri !== undefined) {
search_query = decodeURIComponent(search_query_uri.replace(/\+/g, "%20"));
searchbox.val(search_query);
}

update_search_box();
});
2 changes: 1 addition & 1 deletion assets/html/themes/documenter-dark.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/html/themes/documenter-light.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/html/HTMLWriter.jl
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ function render(doc::Documenter.Document, settings::HTML=HTML())
if isfile(joinpath(doc.user.source, "assets", "search.js"))
@warn "not creating 'search.js', provided by the user."
else
r = JSDependencies.RequireJS([RD.jquery, RD.lunr, RD.lodash])
r = JSDependencies.RequireJS([RD.jquery, RD.minisearch, RD.lodash])
push!(r, JSDependencies.parse_snippet(joinpath(ASSETS, "search.js")))
JSDependencies.verify(r; verbose=true) || error("RequireJS declaration is invalid")
JSDependencies.writejs(joinpath(doc.user.build, "assets", "search.js"), r)
Expand Down
Loading

0 comments on commit c0cfded

Please sign in to comment.