Skip to content

Commit 7567abe

Browse files
committed
Integrate experimental in-browser search #1165
@sabine fix scrolling to element on current page 6476d5d
1 parent 5d7afbf commit 7567abe

File tree

10 files changed

+386
-12
lines changed

10 files changed

+386
-12
lines changed

LICENSE-3RD-PARTY

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ URL: https://swiperjs.com/
112112
License: MIT License
113113
Copyright: Copyright 2014-2021 Vladimir Kharlampidi
114114

115+
asset/vendors/minisearch.min.js:
116+
117+
Name: Minisearch
118+
Version: 6.0.1
119+
URL: https://github.com/lucaong/minisearch
120+
License: MIT License
121+
Copyright: Copyright 2022 Luca Ongaro
122+
115123
--------------------------------------------------------------------------
116124
Licenses
117125
--------------------------------------------------------------------------

asset/vendors/minisearch.min.js

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/global/url.ml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ module Package : sig
1212

1313
val file :
1414
?hash:string -> ?version:string -> filepath:string -> string -> string
15+
16+
val search_index : ?version:string -> digest:string -> string -> string
1517
end = struct
1618
let with_hash = Option.fold ~none:"/p" ~some:(( ^ ) "/u/")
1719
let with_version = Option.fold ~none:"/latest" ~some:(( ^ ) "/")
@@ -25,6 +27,7 @@ end = struct
2527
base ?hash ?version ("/doc/" ^ page)
2628

2729
let file ?hash ?version ~filepath = base ?hash ?version ("/" ^ filepath)
30+
let search_index ?version ~digest = base ?version ("/search-index/" ^ digest)
2831
end
2932

3033
let sitemap = "/sitemap.xml"
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#in-package-search-results ol {
2+
@apply m-0 p-0 list-none;
3+
}
4+
5+
#in-package-search-results li {
6+
@apply m-0 p-0 border-b;
7+
}
8+
9+
#in-package-search-results br {
10+
@apply hidden;
11+
}
12+
13+
#in-package-search-results .search-entry {
14+
@apply px-4 py-1 font-normal text-body-600 flex flex-col;
15+
--entry-color: gray;
16+
}
17+
18+
#in-package-search-results .search-entry:hover {
19+
text-decoration: none;
20+
background: #f4f4f4;
21+
}
22+
23+
@media (min-width: 64em) {
24+
#in-package-search-results .search-entry {
25+
@apply flex-row;
26+
}
27+
28+
#in-package-search-results .search-entry .entry-title {
29+
@apply shrink-0 w-1/2;
30+
}
31+
}
32+
33+
#in-package-search-results .search-entry .entry-kind {
34+
@apply py-0.5 px-1 bg-body-600 text-white rounded mr-2 text-sm;
35+
}
36+
37+
#in-package-search-results .search-entry .entry-kind {
38+
background: var(--entry-color);
39+
}
40+
41+
#in-package-search-results .search-entry.mod {
42+
--entry-color: rgb(204, 78, 12);
43+
}
44+
45+
#in-package-search-results .search-entry.mty {
46+
--entry-color: #027491;
47+
}
48+
49+
#in-package-search-results .search-entry.mtd {
50+
--entry-color: #327d8e;
51+
}
52+
53+
#in-package-search-results .search-entry.typ {
54+
--entry-color: rgb(42, 84, 183);
55+
}
56+
57+
#in-package-search-results .search-entry.val {
58+
--entry-color: green;
59+
}
60+
61+
#in-package-search-results .search-entry.par {
62+
--entry-color: green;
63+
}
64+
65+
#in-package-search-results .search-entry.cls {
66+
--entry-color: rgb(163, 34, 34);
67+
}
68+
69+
#in-package-search-results .search-entry.cty {
70+
--entry-color: rgb(32, 68, 165);
71+
}
72+
73+
#in-package-search-results .search-entry.exc {
74+
--entry-color: #370c62;
75+
}
76+
77+
#in-package-search-results .search-entry.man {
78+
--entry-color: #47424b;
79+
}
80+
81+
#in-package-search-results .search-entry .entry-title {
82+
overflow-wrap: anywhere;
83+
}
84+
85+
#in-package-search-results .search-entry .entry-name {
86+
@apply font-bold;
87+
color: var(--entry-color);
88+
}
89+
90+
#in-package-search-results .search-entry .prefix-name {
91+
@apply font-semibold;
92+
}
93+
94+
#in-package-search-results .search-entry .entry-comment {
95+
@apply whitespace-nowrap overflow-hidden text-ellipsis;
96+
}
97+
98+
#in-package-search-results .active {
99+
@apply bg-primary-100;
100+
}
101+
102+
103+
#in-package-search #in-package-search-results {
104+
@apply hidden;
105+
}
106+
107+
#in-package-search:focus-within #in-package-search-results {
108+
@apply block;
109+
}

src/ocamlorg_frontend/css/styles.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
@import "./partials/cards.css";
77
@import "./partials/forms.css";
88
@import "./partials/grid_logos.css";
9+
@import "./partials/in_package_search.css";
910
@import "./partials/search.css";
1011
@import "./partials/package_breadcrumbs.css";
1112
@import "./partials/shadows.css";

src/ocamlorg_frontend/layouts/package_layout.eml

Lines changed: 192 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ let render
88
?page
99
~(package : Package.package)
1010
~(documentation_status: Package.documentation_status)
11+
~(search_index_digest: string option)
1112
~left_sidebar_html
1213
~right_sidebar_html
1314
inner =
@@ -60,13 +61,21 @@ Layout.render
6061
</li>
6162
</ol>
6263

63-
<div title="Sorry, in-package search is not yet implemented, but this is where it's going to appear." class="flex w-full items-center">
64-
<input disabled type="search" name="q" class="focus:border-gray-800 text-gray-800 bg-gray-300 border-gray-300 h-10 rounded-l-md appearance-none px-4 flex-grow"
65-
autocomplete="off"
66-
placeholder="Sorry, in-package search is not yet implemented, but this is where it's going to appear :-)">
67-
<button disabled aria-label="search" class="h-10 rounded-r-md bg-gray-300 text-gray-800 flex items-center justify-center px-4">
68-
<%s! Icons.magnifying_glass "w-6 h-6" %>
69-
</button>
64+
<div id="in-package-search" class="relative w-full">
65+
<div class="flex w-full items-center">
66+
<% if Option.is_some search_index_digest then (%>
67+
<input id="in-package-search-input" type="search" name="q" class="focus:border-gray-800 text-gray-800 border-primary-600 h-10 rounded-l-md appearance-none px-4 flex-grow"
68+
autocomplete="off"
69+
placeholder="Search names in this package..."
70+
>
71+
<div aria-hidden="true" class="h-10 rounded-r-md bg-primary-600 text-white flex items-center justify-center px-4">
72+
<%s! Icons.magnifying_glass "w-6 h-6" %>
73+
</div>
74+
<% ); %>
75+
</div>
76+
77+
<div id="in-package-search-results" class="absolute top-12 right-0 left-0 bg-white z-20 w-full max-h-[60vh] overflow-y-auto border rounded-lg shadow-xl">
78+
</div>
7079
</div>
7180
</div>
7281

@@ -78,6 +87,7 @@ Layout.render
7887
x-transition:leave-end="-translate-x-full">
7988
<%s! left_sidebar_html %>
8089
</div>
90+
8191
<div class="flex-1 z-0 z- min-w-0 pt-6 pb-12 md:pb-[70vh]">
8292
<%s! inner %>
8393
</div>
@@ -91,4 +101,178 @@ Layout.render
91101
</div>
92102
</div>
93103
</div>
94-
<%s! Toc.script %>
104+
<%s! Toc.script %>
105+
<% (match search_index_digest with | Some(digest) -> %>
106+
<script src="<%s Ocamlorg_static.Asset. url "vendors/minisearch.min.js" %>"></script>
107+
<script>
108+
function Fuse() {}
109+
</script>
110+
<script>
111+
{
112+
let miniSearch;
113+
let results = [];
114+
115+
function shortness_factor(r) {
116+
return 1 + 2*Math.log(1 + 1/(r.prefixname.length + r.name.length));
117+
}
118+
119+
function perform_search() {
120+
let q = document.getElementById("in-package-search-input").value;
121+
results = miniSearch.search(q, {
122+
fields: ['name', 'prefixname', 'comment'],
123+
prefix: true,
124+
boost: {
125+
name: 6,
126+
prefixname: 2.5,
127+
comment: 0.8,
128+
},
129+
fuzzy: 0.15,
130+
}).slice(0,50);
131+
results = results.map(r => {return {...r, score: r.score * shortness_factor(r), shortness: shortness_factor(r)}}).sort((r1,r2) => r2.score - r1.score);
132+
console.log("results", results);
133+
let container = document.getElementById("in-package-search-results");
134+
container.innerHTML = "";
135+
136+
let search_results = document.createElement("ol");
137+
138+
results.map((entry) => {
139+
let kind = document.createElement("tt");
140+
kind.innerText = entry.kind
141+
.replace("module type", "mty").replace("root", "mod").replace("module", "mod")
142+
.replace("method", "mtd").replace("class type", "cty").replace("class", "cls")
143+
.replace("core type", "typ").replace("type", "typ")
144+
.replace("exception", "exc").replace("core exception", "exc")
145+
.replace("parameter", "par")
146+
.replace("leaf page", "man").replace("page", "man");
147+
kind.title = entry.kind;
148+
kind.classList.add("entry-kind");
149+
150+
let list_item = document.createElement("li");
151+
let a = document.createElement("a");
152+
a.href = "/" + entry.url;
153+
a.id = "search-result-"+entry.id;
154+
a.classList.add("search-entry", kind.innerText.slice(0,3));
155+
let title = document.createElement("div");
156+
title.classList.add("entry-title");
157+
158+
let prefixname = document.createElement("tt");
159+
prefixname.innerText = entry.prefixname.split(".").reverse().join(".") + (entry.prefixname != "" ? ".": "");
160+
prefixname.classList.add("prefix-name");
161+
let name = document.createElement("tt");
162+
name.classList.add("entry-name");
163+
name.innerText = entry.name;
164+
165+
title.appendChild(kind);
166+
title.appendChild(prefixname);
167+
title.appendChild(name);
168+
169+
let comment = document.createElement("div");
170+
comment.innerText = entry.comment;
171+
comment.classList.add("entry-comment");
172+
173+
a.appendChild(title);
174+
a.appendChild(comment);
175+
176+
a.addEventListener("click", () => scrollTo(a.href));
177+
178+
list_item.appendChild(a);
179+
180+
search_results.appendChild(list_item);
181+
});
182+
container.appendChild(search_results);
183+
184+
search_results_position = null;
185+
}
186+
187+
function init_search() {
188+
documents = documents.map((d, i) => {return {...d, id: i}});
189+
190+
miniSearch = new MiniSearch({
191+
fields: ['name', 'prefixname', 'comment'],
192+
storeFields: ['name', 'prefixname', 'kind', 'url', 'comment'],
193+
});
194+
195+
miniSearch.addAll(documents);
196+
197+
document.getElementById("in-package-search-input").addEventListener("input", perform_search);
198+
perform_search();
199+
}
200+
201+
function user_interacts() {
202+
let scriptTag = document.createElement("script");
203+
scriptTag.src = "<%s Url.Package.search_index ?version:(Package.url_version package) package.name ~digest %>";
204+
scriptTag.addEventListener("load", init_search);
205+
document.body.appendChild(scriptTag);
206+
207+
document.getElementById("in-package-search-input").removeEventListener("focus", user_interacts);
208+
}
209+
210+
document.getElementById("in-package-search-input").addEventListener("focus", user_interacts);
211+
212+
213+
let search_results_position = null;
214+
function adjust_position(event) {
215+
if (results.length == 0) return;
216+
217+
if (event.key == "ArrowDown") {
218+
if (search_results_position === null) {
219+
search_results_position = 0;
220+
return;
221+
}
222+
if (search_results_position < results.length - 1) {
223+
search_results_position++;
224+
return;
225+
}
226+
} else if (event.key == "ArrowUp") {
227+
if (search_results_position === null) return;
228+
229+
if (search_results_position == 0) {
230+
search_results_position = null;
231+
return;
232+
}
233+
234+
search_results_position--;
235+
return;
236+
}
237+
}
238+
239+
function keydown(event) {
240+
event.stopPropagation();
241+
if (event.key == "Enter") {
242+
if (results.length > 0) {
243+
let url = "/"+results[search_results_position || 0].url;
244+
console.log("going to", url);
245+
window.location = url;
246+
247+
scrollTo(url);
248+
249+
document.getElementById("in-package-search-input").value = "";
250+
results = [];
251+
search_results_position = null;
252+
}
253+
return false;
254+
}
255+
256+
if (search_results_position !== null) document.getElementById("search-result-"+results[search_results_position].id).classList.remove("active");
257+
adjust_position(event);
258+
if (search_results_position !== null) {
259+
let el = document.getElementById("search-result-"+results[search_results_position].id);
260+
el.classList.add("active");
261+
el.scrollIntoView({block: "end"});
262+
}
263+
}
264+
265+
function scrollTo(url) {
266+
if (url.indexOf("#") != -1) {
267+
let id = url.split("#")[1]
268+
let el = document.getElementById(id);
269+
console.log("id el", id, el);
270+
if (el) setTimeout(() => {
271+
console.log("scroll"); el.scrollIntoView();}, 10);
272+
}
273+
}
274+
275+
document.getElementById("in-package-search-input").addEventListener("keydown", keydown);
276+
}
277+
</script>
278+
<% | _ -> () ); %>

src/ocamlorg_package/lib/ocamlorg_package.ml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,19 @@ let file ~kind t path =
351351
let url = package_url ^ path ^ ".json" in
352352
odoc_page ~url
353353

354+
let search_index ~kind t =
355+
let package_url =
356+
package_url ~kind (Name.to_string t.name) (Version.to_string t.version)
357+
in
358+
let url = package_url ^ "index.js" in
359+
let open Lwt.Syntax in
360+
let* content = http_get url in
361+
match content with
362+
| Ok content -> Lwt.return (Some content)
363+
| Error _ ->
364+
Logs.info (fun m -> m "Failed to fetch search index at %s" url);
365+
Lwt.return None
366+
354367
let maybe_file ~kind t filename =
355368
let open Lwt.Syntax in
356369
let+ doc = file ~kind t filename in

0 commit comments

Comments
 (0)