Skip to content

Commit 9d0497b

Browse files
authored
Basic search (pulumi#418)
1 parent 283d89a commit 9d0497b

8 files changed

+403
-4
lines changed

_config.yml

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ exclude:
2929
- .gitattributes
3030
- .gitignore
3131
- .travis.yml
32+
- CODE-OF-CONDUCT.md
33+
- CONTRIBUTING.md
34+
- Dockerfile
3235
- infrastructure
3336
- Gemfile
3437
- Gemfile.lock

_includes/header.html

+3-4
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,9 @@
5353
</button>
5454

5555
<span>
56-
<!-- TODO[pulumi/docs#296]: Enable search in docs. -->
57-
<form id="search-form">
58-
<input type="search" placeholder="Search...">
59-
<button><i class="fas fa-search"></i></button>
56+
<form id="search-form" action="/search.html" method="get">
57+
<input type="search" id="search-box" name="q" placeholder="Search..." autocomplete="off">
58+
<button type="submit"><i class="fas fa-search"></i></button>
6059
</form>
6160
</span>
6261
</div>

css/style.scss

+69
Original file line numberDiff line numberDiff line change
@@ -643,3 +643,72 @@ h4 { font-size: 22px; font-weight: 500; }
643643
text-decoration: none;
644644
}
645645
}
646+
647+
.loader {
648+
border: 10px solid #f3f3f3;
649+
border-top: 10px solid $primary2;
650+
border-radius: 50%;
651+
width: 50px;
652+
height: 50px;
653+
animation: spin 1s linear infinite;
654+
box-sizing: content-box;
655+
}
656+
657+
@keyframes spin {
658+
0% { transform: rotate(0deg); }
659+
100% { transform: rotate(360deg); }
660+
}
661+
662+
#search-results-container {
663+
display: flex;
664+
flex-direction: row;
665+
justify-content: space-around;
666+
overflow: auto;
667+
width: auto;
668+
}
669+
670+
@media (max-width: 480px) {
671+
#search-results-container {
672+
display: block;
673+
}
674+
}
675+
676+
.search-results ul li {
677+
padding-left: 0px;
678+
}
679+
680+
.search-results ul li.top {
681+
font-weight: bold;
682+
}
683+
684+
.search-results ul li:before {
685+
content: '';
686+
}
687+
688+
.symbol {
689+
border-radius: 2px;
690+
color: #fff;
691+
display: inline-block;
692+
font-size: 11px;
693+
font-weight: 600;
694+
line-height: 18px;
695+
text-align: center;
696+
width: 18px;
697+
margin-right: 8px;
698+
}
699+
700+
.symbol.module {
701+
background: #2fc89f;
702+
}
703+
704+
.symbol.module:before {
705+
content: "M";
706+
}
707+
708+
.symbol.package {
709+
background: #512668;
710+
}
711+
712+
.symbol.package:before {
713+
content: "Pk";
714+
}

js/lunr.min.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js/search-worker.js

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"use strict";
2+
3+
importScripts("/js/lunr.min.js");
4+
5+
var data = {};
6+
var index;
7+
8+
// Download the search data synchronously.
9+
var req = new XMLHttpRequest();
10+
var async = false;
11+
req.open("GET", "/search-data.json", async);
12+
req.send();
13+
if (req.readyState === req.DONE && req.status === 200) {
14+
data = JSON.parse(req.responseText);
15+
loadIndex();
16+
} else {
17+
console.log("problem downloading search-data.json");
18+
}
19+
20+
function loadIndex() {
21+
index = lunr(function() {
22+
this.ref("id");
23+
this.field("title", { boost: 10 });
24+
this.field("content");
25+
26+
for (var key in data) {
27+
var item = data[key];
28+
if (item.title || item.content) {
29+
this.add({
30+
id: key,
31+
title: data[key].title,
32+
content: data[key].content
33+
});
34+
}
35+
}
36+
});
37+
}
38+
39+
function search(query) {
40+
try {
41+
if (index && query.length) {
42+
var results = index.search(query);
43+
44+
return results.map(function(result) {
45+
return {
46+
score: result.score,
47+
url: result.ref,
48+
title: data[result.ref].title
49+
};
50+
});
51+
}
52+
} catch (e) {
53+
console.log(e);
54+
}
55+
return [];
56+
}
57+
58+
self.onmessage = function (message) {
59+
var type = message.data.type;
60+
var payload = message.data.payload;
61+
if (type === "search") {
62+
self.postMessage({ payload: { query: payload, results: search(payload) } });
63+
}
64+
}

js/search.js

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
"use strict";
2+
3+
(function () {
4+
var searchForm = document.getElementById("search-form");
5+
var searchBox = document.getElementById("search-box");
6+
var spinner = document.getElementById("spinner");
7+
var searchResultsContainer = document.getElementById("search-results-container");
8+
9+
// Use a worker to create the index in the background.
10+
var worker = new Worker("/js/search-worker.js");
11+
worker.onmessage = function (message) {
12+
var payload = message.data.payload;
13+
displaySearchResults(payload.results);
14+
};
15+
16+
// Extract the query from the browser's URL, set the search-box to the query,
17+
// and kick-off the search by sending a message to the worker.
18+
var query = getQueryVariable("q");
19+
if (query) {
20+
searchBox.value = query;
21+
sendSearchMessage(query);
22+
} else {
23+
displaySearchResults([]);
24+
}
25+
26+
// To speed up subsequent searches on this page, if the browser supports pushState,
27+
// add a listener to the search form's submit event that prevents the browser from
28+
// doing an actual GET request navigation on searches. Instead, we just proceed with
29+
// the new search without navigating and update the browser location with the new
30+
// query string programmatically.
31+
if (history.pushState) {
32+
searchForm.addEventListener("submit", function (e) {
33+
e.preventDefault();
34+
35+
var query = searchBox.value;
36+
if (query) {
37+
// Update the browser's location with the new query string.
38+
var newurl = window.location.protocol + "//" + window.location.host +
39+
window.location.pathname + "?q=" + encodeURIComponent(query);
40+
history.pushState({ path: newurl }, "", newurl);
41+
42+
// Kick-off the new search.
43+
sendSearchMessage(query);
44+
}
45+
46+
return false;
47+
});
48+
}
49+
50+
// If we're navigating back/forward from a history.pushState,
51+
// then intercept and search for the term again.
52+
window.onpopstate = function (event) {
53+
var query = getQueryVariable("q");
54+
if (query) {
55+
searchBox.value = query;
56+
sendSearchMessage(query);
57+
}
58+
}
59+
60+
// Extracts a query string variable from the browser's location.
61+
function getQueryVariable(variable) {
62+
var query = window.location.search.substring(1);
63+
var vars = query.split("&");
64+
65+
for (var i = 0; i < vars.length; i++) {
66+
var pair = vars[i].split("=");
67+
68+
if (pair[0] === variable) {
69+
return decodeURIComponent(pair[1].replace(/\+/g, "%20"));
70+
}
71+
}
72+
}
73+
74+
// Sends a message to the worker to kick-off a search.
75+
function sendSearchMessage(query) {
76+
worker.postMessage({ type: "search", payload: query });
77+
}
78+
79+
// Display the results of the search.
80+
function displaySearchResults(results) {
81+
// Hide the spinner.
82+
spinner.style.display = "none";
83+
84+
if (results.length) {
85+
// Group the results by category.
86+
var categoryMap = {};
87+
for (var i = 0; i < results.length; i++) {
88+
var result = results[i];
89+
var categoryName = getCategoryName(result.url);
90+
var category = categoryMap[categoryName] = categoryMap[categoryName] || [];
91+
prepareResult(result, categoryName);
92+
category.push(result);
93+
}
94+
95+
// Build up the HTML to display.
96+
var appendString = "";
97+
for (var i = 0; i < categories.length; i++) {
98+
var categoryName = categories[i].name;
99+
var category = categoryMap[categoryName];
100+
if (category && category.length > 0) {
101+
appendString += buildCategoryString(categoryName, category);
102+
}
103+
}
104+
searchResultsContainer.innerHTML = appendString;
105+
} else {
106+
searchResultsContainer.innerHTML = "<p>No results found.</p>";
107+
}
108+
}
109+
110+
var defaultCategory = "Other";
111+
var categories = [
112+
{
113+
name: "APIs",
114+
predicate: function (url) {
115+
return url.startsWith("/reference/pkg/");
116+
}
117+
},
118+
{
119+
name: "CLI",
120+
predicate: function (url) {
121+
return url.startsWith("/reference/cli/") || url === "/reference/commands.html";
122+
}
123+
},
124+
{
125+
name: "Tutorials",
126+
predicate: function (url) {
127+
return url.startsWith("/quickstart/");
128+
}
129+
},
130+
{
131+
name: defaultCategory,
132+
predicate: function (url) {
133+
return true;
134+
}
135+
},
136+
];
137+
138+
function getCategoryName(url) {
139+
for (var i = 0; i < categories.length; i++) {
140+
var category = categories[i];
141+
if (category.predicate(url)) {
142+
return category.name;
143+
}
144+
}
145+
return defaultCategory;
146+
}
147+
148+
function prepareResult(result, categoryName) {
149+
switch (categoryName) {
150+
case "APIs":
151+
if (result.title.startsWith("Module ")) {
152+
result.display = result.title.substring("Module ".length);
153+
result.type = "module";
154+
return;
155+
} else if (result.title.startsWith("Package ")) {
156+
result.display = result.title.substring("Package ".length);
157+
result.type = "package";
158+
return;
159+
}
160+
break;
161+
162+
case "CLI":
163+
if (result.title.length === 0 && result.url.startsWith("/reference/cli/")) {
164+
var regex = /\/reference\/cli\/([a-z_]+).html/gm;
165+
var match = regex.exec(result.url)
166+
if (match !== null) {
167+
result.display = match[1].replace(/_/g, " ");
168+
return;
169+
}
170+
}
171+
break;
172+
}
173+
174+
result.display = result.title || result.url;
175+
}
176+
177+
function buildCategoryString(categoryName, category) {
178+
var appendString = "<div class='search-results'><h3>" + categoryName + " (" + category.length + ")</h3>";
179+
180+
// Display the top 5 results first.
181+
appendString += "<ul>";
182+
var topResults = category.splice(0, 5);
183+
for (var i = 0; i < topResults.length; i++) {
184+
var item = topResults[i];
185+
var prefix = getPrefix(item, categoryName);
186+
appendString += "<li class='top'><a href='" + item.url + "'>" + prefix + item.display + "</a>";
187+
}
188+
appendString += "</ul>";
189+
190+
// Now display any remaining results, sorted alphabetically.
191+
if (category.length > 0) {
192+
category.sort(function (l, r) {
193+
return l.display.toUpperCase() > r.display.toUpperCase() ? 1 : -1;
194+
});
195+
appendString += "<ul>";
196+
for (var i = 0; i < category.length; i++) {
197+
var item = category[i];
198+
var prefix = getPrefix(item, categoryName);
199+
appendString += "<li><a href='" + item.url + "'>" + prefix + item.display + "</a>";
200+
}
201+
appendString += "</ul>";
202+
}
203+
204+
appendString += "</div>";
205+
return appendString;
206+
}
207+
208+
function getPrefix(result, categoryName) {
209+
if (result.type) {
210+
switch (result.type) {
211+
case "module":
212+
return "<span class='symbol module'></span>";
213+
case "package":
214+
return "<span class='symbol package'></span>"
215+
}
216+
}
217+
218+
return "";
219+
}
220+
})();

0 commit comments

Comments
 (0)