Fork of daniellemaywood.uk/gleam — Wasm codegen work
20 kB
643 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2021 The Gleam contributors
3
4"use strict";
5
6window.Gleam = (function () {
7 /* Global Object */
8 const self = {};
9
10 /* Public Properties */
11
12 self.hashOffset = undefined;
13
14 /* Public Methods */
15
16 self.getProperty = function (property) {
17 let value;
18 try {
19 value = localStorage.getItem(`Gleam.${property}`);
20 } catch (_error) { }
21 if (-1 < [null, undefined].indexOf(value)) {
22 return gleamConfig[property].values[0].value;
23 }
24 return value;
25 };
26
27 self.icons = function () {
28 return Array.from(arguments).reduce(
29 (acc, name) =>
30 `${acc}
31 <svg class="icon icon-${name}"><use xlink:href="#icon-${name}"></use></svg>`,
32 "",
33 );
34 };
35
36 self.scrollToHash = function () {
37 const locationHash = arguments[0] || window.location.hash;
38 const query = locationHash ? locationHash : "body";
39 const hashTop = document.querySelector(query).offsetTop;
40 window.scrollTo(0, hashTop - self.hashOffset);
41 return locationHash;
42 };
43
44 self.toggleSidebar = function () {
45 const previousState = bodyClasses.contains("drawer-open")
46 ? "open"
47 : "closed";
48
49 let state;
50 if (0 < arguments.length) {
51 state = false === arguments[0] ? "closed" : "open";
52 } else {
53 state = "open" === previousState ? "closed" : "open";
54 }
55
56 bodyClasses.remove(`drawer-${previousState}`);
57 bodyClasses.add(`drawer-${state}`);
58
59 if ("open" === state) {
60 document.addEventListener("click", closeSidebar, false);
61 }
62 };
63
64 /* Private Properties */
65
66 const html = document.documentElement;
67 const body = document.body;
68 const bodyClasses = body.classList;
69 const sidebar = document.querySelector(".sidebar");
70 const sidebarToggles = document.querySelectorAll(".sidebar-toggle");
71 const displayControls = document.createElement("div");
72
73 const isMac = navigator?.userAgentData?.platform === 'macOS'
74 || /mac/i.test(navigator.userAgent);
75
76 displayControls.classList.add("display-controls");
77 sidebar.appendChild(displayControls);
78
79 /* Private Methods */
80
81 const initProperty = function (property) {
82 const config = gleamConfig[property];
83
84 displayControls.insertAdjacentHTML(
85 "beforeend",
86 config.values.reduce(
87 (acc, item, index) => {
88 const tooltip = item.label
89 ? `alt="${item.label}" title="${item.label}"`
90 : "";
91 let inner;
92 if (item.icons) {
93 inner = self.icons(...item.icons);
94 } else if (item.label) {
95 inner = item.label;
96 } else {
97 inner = "";
98 }
99 return `
100 ${acc}
101 <span class="label label-${index}" ${tooltip}>
102 ${inner}
103 </span>
104 `;
105 },
106 `<button
107 id="${property}-toggle"
108 class="control control-${property} toggle toggle-0">
109 `,
110 ) +
111 `
112 </button>
113 `,
114 );
115
116 setProperty(null, property, function () {
117 return self.getProperty(property);
118 });
119 };
120
121 const setProperty = function (_event, property) {
122 const previousValue = self.getProperty(property);
123
124 const update =
125 2 < arguments.length ? arguments[2] : gleamConfig[property].update;
126 const value = update();
127
128 try {
129 localStorage.setItem("Gleam." + property, value);
130 } catch (_error) { }
131
132 bodyClasses.remove(`${property}-${previousValue}`);
133 bodyClasses.add(`${property}-${value}`);
134
135 const isDefault = value === gleamConfig[property].values[0].value;
136 const toggleClasses = document.querySelector(
137 `#${property}-toggle`,
138 ).classList;
139 toggleClasses.remove(`toggle-${isDefault ? 1 : 0}`);
140 toggleClasses.add(`toggle-${isDefault ? 0 : 1}`);
141
142 try {
143 gleamConfig[property].callback(value);
144 } catch (_error) { }
145
146 return value;
147 };
148
149 const setHashOffset = function () {
150 const el = document.createElement("div");
151 el.style.cssText = `
152 height: var(--hash-offset);
153 pointer-events: none;
154 position: absolute;
155 visibility: hidden;
156 width: 0;
157 `;
158 body.appendChild(el);
159 self.hashOffset = parseInt(
160 getComputedStyle(el).getPropertyValue("height") || "0",
161 );
162 body.removeChild(el);
163 };
164
165 const closeSidebar = function (event) {
166 if (!event.target.closest(".sidebar-toggle")) {
167 document.removeEventListener("click", closeSidebar, false);
168 self.toggleSidebar(false);
169 }
170 };
171
172 const addEvent = function (el, type, handler) {
173 if (el.attachEvent) el.attachEvent("on" + type, handler);
174 else el.addEventListener(type, handler);
175 };
176
177 const searchLoaded = function (index, searchItems) {
178 const preview_words_after = 10;
179 const preview_words_before = 5;
180 const previews = 3;
181
182 const searchInput = document.getElementById("search-input");
183 const searchNavButton = document.getElementById("search-nav-button");
184 const searchResults = document.getElementById("search-results");
185 let currentInput;
186 let currentSearchIndex = 0;
187
188 // Set search input placeholder based on OS
189 searchInput.setAttribute("placeholder", isMac ? "Search ⌘ + K" : "Search Ctrl + K")
190
191 function showSearch() {
192 document.documentElement.classList.add("search-active");
193 }
194
195 searchNavButton.addEventListener("click", function (e) {
196 e.stopPropagation();
197 showSearch();
198 setTimeout(function () {
199 searchInput.focus();
200 }, 0);
201 });
202
203 function hideSearch() {
204 document.documentElement.classList.remove("search-active");
205 }
206
207 function update() {
208 currentSearchIndex++;
209
210 const input = searchInput.value;
211 showSearch();
212 if (input === currentInput) {
213 return;
214 }
215 currentInput = input;
216 searchResults.innerHTML = "";
217 if (input === "") {
218 return;
219 }
220
221 let results = index.query(function (query) {
222 const tokens = lunr.tokenizer(input);
223 query.term(tokens, {
224 boost: 10,
225 });
226 query.term(tokens, {
227 boost: 5,
228 wildcard: lunr.Query.wildcard.TRAILING,
229 });
230 query.term(tokens, {
231 wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING,
232 });
233 });
234
235 if (results.length == 0 && input.length > 2) {
236 const tokens = lunr.tokenizer(input).filter(function (token, i) {
237 return token.str.length < 20;
238 });
239 if (tokens.length > 0) {
240 results = index.query(function (query) {
241 query.term(tokens, {
242 editDistance: Math.round(Math.sqrt(input.length / 2 - 1)),
243 });
244 });
245 }
246 }
247
248 if (results.length == 0) {
249 const noResultsDiv = document.createElement("div");
250 noResultsDiv.classList.add("search-no-result");
251 noResultsDiv.innerText = "No results found";
252 searchResults.appendChild(noResultsDiv);
253 } else {
254 const resultsList = document.createElement("ul");
255 resultsList.classList.add("search-results-list");
256 searchResults.appendChild(resultsList);
257
258 addResults(resultsList, results, 0, 10, 100, currentSearchIndex);
259 }
260
261 function addResults(
262 resultsList,
263 results,
264 start,
265 batchSize,
266 batchMillis,
267 searchIndex,
268 ) {
269 if (searchIndex != currentSearchIndex) {
270 return;
271 }
272 for (let i = start; i < start + batchSize; i++) {
273 if (i == results.length) {
274 return;
275 }
276 addResult(resultsList, results[i]);
277 }
278 setTimeout(function () {
279 addResults(
280 resultsList,
281 results,
282 start + batchSize,
283 batchSize,
284 batchMillis,
285 searchIndex,
286 );
287 }, batchMillis);
288 }
289
290 function addResult(resultsList, result) {
291 const searchItem = searchItems[result.ref];
292 const resultsListItem = document.createElement("li");
293 resultsListItem.classList.add("search-results-list-item");
294 resultsList.appendChild(resultsListItem);
295 const resultLink = document.createElement("a");
296 resultLink.classList.add("search-result");
297 resultLink.setAttribute("href", `${window.unnest}/${searchItem.ref}`);
298 resultsListItem.appendChild(resultLink);
299 const resultTitle = document.createElement("div");
300 resultTitle.classList.add("search-result-title");
301 resultLink.appendChild(resultTitle);
302 const resultDoc = document.createElement("div");
303 resultDoc.classList.add("search-result-doc");
304 resultDoc.innerHTML =
305 '<svg viewBox="0 0 24 24" class="search-result-icon"><use xlink:href="#icon-svg-doc"></use></svg>';
306 resultTitle.appendChild(resultDoc);
307 const resultDocTitle = document.createElement("div");
308 resultDocTitle.classList.add("search-result-doc-title");
309 resultDocTitle.innerHTML = searchItem.parentTitle;
310 resultDoc.appendChild(resultDocTitle);
311 let resultDocOrSection = resultDocTitle;
312 if (searchItem.parentTitle != searchItem.title) {
313 resultDoc.classList.add("search-result-doc-parent");
314 const resultSection = document.createElement("div");
315 resultSection.classList.add("search-result-section");
316 resultSection.innerHTML = searchItem.title;
317 resultTitle.appendChild(resultSection);
318 resultDocOrSection = resultSection;
319 }
320 const metadata = result.matchData.metadata;
321 const titlePositions = [];
322 const contentPositions = [];
323 for (let j in metadata) {
324 const meta = metadata[j];
325 if (meta.title) {
326 const positions = meta.title.position;
327 for (let k in positions) {
328 titlePositions.push(positions[k]);
329 }
330 }
331 if (meta.content) {
332 const positions = meta.content.position;
333 for (let k in positions) {
334 const position = positions[k];
335 let previewStart = position[0];
336 let previewEnd = position[0] + position[1];
337 let ellipsesBefore = true;
338 let ellipsesAfter = true;
339 for (let k = 0; k < preview_words_before; k++) {
340 const nextSpace = searchItem.doc.lastIndexOf(
341 " ",
342 previewStart - 2,
343 );
344 const nextDot = searchItem.doc.lastIndexOf(". ", previewStart - 2);
345 if (nextDot >= 0 && nextDot > nextSpace) {
346 previewStart = nextDot + 1;
347 ellipsesBefore = false;
348 break;
349 }
350 if (nextSpace < 0) {
351 previewStart = 0;
352 ellipsesBefore = false;
353 break;
354 }
355 previewStart = nextSpace + 1;
356 }
357 for (let k = 0; k < preview_words_after; k++) {
358 const nextSpace = searchItem.doc.indexOf(" ", previewEnd + 1);
359 const nextDot = searchItem.doc.indexOf(". ", previewEnd + 1);
360 if (nextDot >= 0 && nextDot < nextSpace) {
361 previewEnd = nextDot;
362 ellipsesAfter = false;
363 break;
364 }
365 if (nextSpace < 0) {
366 previewEnd = searchItem.doc.length;
367 ellipsesAfter = false;
368 break;
369 }
370 previewEnd = nextSpace;
371 }
372 contentPositions.push({
373 highlight: position,
374 previewStart: previewStart,
375 previewEnd: previewEnd,
376 ellipsesBefore: ellipsesBefore,
377 ellipsesAfter: ellipsesAfter,
378 });
379 }
380 }
381 }
382 if (titlePositions.length > 0) {
383 titlePositions.sort(function (p1, p2) {
384 return p1[0] - p2[0];
385 });
386 resultDocOrSection.innerHTML = "";
387 addHighlightedText(
388 resultDocOrSection,
389 searchItem.title,
390 0,
391 searchItem.title.length,
392 titlePositions,
393 );
394 }
395 if (contentPositions.length > 0) {
396 contentPositions.sort(function (p1, p2) {
397 return p1.highlight[0] - p2.highlight[0];
398 });
399 let contentPosition = contentPositions[0];
400 let previewPosition = {
401 highlight: [contentPosition.highlight],
402 previewStart: contentPosition.previewStart,
403 previewEnd: contentPosition.previewEnd,
404 ellipsesBefore: contentPosition.ellipsesBefore,
405 ellipsesAfter: contentPosition.ellipsesAfter,
406 };
407 const previewPositions = [previewPosition];
408 for (let j = 1; j < contentPositions.length; j++) {
409 contentPosition = contentPositions[j];
410 if (previewPosition.previewEnd < contentPosition.previewStart) {
411 previewPosition = {
412 highlight: [contentPosition.highlight],
413 previewStart: contentPosition.previewStart,
414 previewEnd: contentPosition.previewEnd,
415 ellipsesBefore: contentPosition.ellipsesBefore,
416 ellipsesAfter: contentPosition.ellipsesAfter,
417 };
418 previewPositions.push(previewPosition);
419 } else {
420 previewPosition.highlight.push(contentPosition.highlight);
421 previewPosition.previewEnd = contentPosition.previewEnd;
422 previewPosition.ellipsesAfter = contentPosition.ellipsesAfter;
423 }
424 }
425 const resultPreviews = document.createElement("div");
426 resultPreviews.classList.add("search-result-previews");
427 resultLink.appendChild(resultPreviews);
428 const content = searchItem.doc;
429 for (
430 let j = 0;
431 j < Math.min(previewPositions.length, previews);
432 j++
433 ) {
434 const position = previewPositions[j];
435 const resultPreview = document.createElement("div");
436 resultPreview.classList.add("search-result-preview");
437 resultPreviews.appendChild(resultPreview);
438 if (position.ellipsesBefore) {
439 resultPreview.appendChild(document.createTextNode("... "));
440 }
441 addHighlightedText(
442 resultPreview,
443 content,
444 position.previewStart,
445 position.previewEnd,
446 position.highlight,
447 );
448 if (position.ellipsesAfter) {
449 resultPreview.appendChild(document.createTextNode(" ..."));
450 }
451 }
452 }
453 const resultRelUrl = document.createElement("span");
454 resultRelUrl.classList.add("search-result-rel-url");
455 resultRelUrl.innerText = searchItem.ref;
456 resultTitle.appendChild(resultRelUrl);
457 }
458
459 function addHighlightedText(parent, text, start, end, positions) {
460 let index = start;
461 for (let i in positions) {
462 const position = positions[i];
463 const span = document.createElement("span");
464 span.innerHTML = text.substring(index, position[0]);
465 parent.appendChild(span);
466 index = position[0] + position[1];
467 const highlight = document.createElement("span");
468 highlight.classList.add("search-result-highlight");
469 highlight.innerHTML = text.substring(position[0], index);
470 parent.appendChild(highlight);
471 }
472 const span = document.createElement("span");
473 span.innerHTML = text.substring(index, end);
474 parent.appendChild(span);
475 }
476 }
477
478 addEvent(document, "keydown", function (event) {
479 // Don't do anything when search is already in focus
480 if (document.activeElement == searchInput) {
481 return
482 }
483
484 if (
485 // Handle cmd/ctrl + k
486 ((event.metaKey || event.ctrlKey) && event.key === 'k') ||
487 // Handle s or /
488 event.key === 's' || event.key === '/'
489 ) {
490 event.preventDefault();
491
492 searchInput.focus();
493 return;
494 }
495 })
496
497 addEvent(searchInput, "focus", function () {
498 setTimeout(update, 0);
499 });
500
501 addEvent(searchInput, "keyup", function (e) {
502 switch (e.keyCode) {
503 case 27: // When esc key is pressed, hide the results and clear the field
504 searchInput.value = "";
505 break;
506 case 38: // arrow up
507 case 40: // arrow down
508 case 13: // enter
509 e.preventDefault();
510 return;
511 }
512 update();
513 });
514
515 addEvent(searchInput, "keydown", function (e) {
516 let active;
517 switch (e.keyCode) {
518 case 38: // arrow up
519 e.preventDefault();
520 active = document.querySelector(".search-result.active");
521 if (active) {
522 active.classList.remove("active");
523 if (active.parentElement.previousSibling) {
524 const previous =
525 active.parentElement.previousSibling.querySelector(
526 ".search-result",
527 );
528 previous.classList.add("active");
529 }
530 }
531 return;
532 case 40: // arrow down
533 e.preventDefault();
534 active = document.querySelector(".search-result.active");
535 if (active) {
536 if (active.parentElement.nextSibling) {
537 const next =
538 active.parentElement.nextSibling.querySelector(
539 ".search-result",
540 );
541 active.classList.remove("active");
542 next.classList.add("active");
543 }
544 } else {
545 const next = document.querySelector(".search-result");
546 if (next) {
547 next.classList.add("active");
548 }
549 }
550 return;
551 case 13: // enter
552 e.preventDefault();
553 active = document.querySelector(".search-result.active");
554 if (active) {
555 active.click();
556 } else {
557 const first = document.querySelector(".search-result");
558 if (first) {
559 first.click();
560 }
561 }
562 return;
563 }
564 });
565
566 addEvent(document, "click", function (e) {
567 if (e.target != searchInput) {
568 hideSearch();
569 }
570 });
571 };
572
573 self.initSearch = function initSeach(searchData) {
574 // enable support for hyphenated search words
575 lunr.tokenizer.separator = /[\s/]+/;
576 const index = lunr(function () {
577 this.ref("id");
578 // this.field("type");
579 // this.field("parentTitle");
580 this.field("title", { boost: 200 });
581 this.field("doc", { boost: 2 });
582 this.field("ref");
583 this.metadataWhitelist = ["position"];
584
585 for (let [i, entry] of searchData.items.entries()) {
586 this.add({
587 id: i,
588 // type: entry.type,
589 parentTitle: entry.parentTitle,
590 title: entry.title,
591 doc: entry.doc,
592 ref: `${window.unnest}/${entry.ref}`,
593 });
594 }
595 });
596 searchLoaded(index, searchData.items);
597 };
598
599 const init = function () {
600 for (let property in gleamConfig) {
601 initProperty(property);
602 const toggle = document.querySelector(`#${property}-toggle`);
603 toggle.addEventListener("click", function (event) {
604 setProperty(event, property);
605 });
606 }
607
608 sidebarToggles.forEach(function (sidebarToggle) {
609 sidebarToggle.addEventListener("click", function (event) {
610 event.preventDefault();
611 self.toggleSidebar();
612 });
613 });
614
615 setHashOffset();
616 window.addEventListener("load", function (_event) {
617 self.scrollToHash();
618 });
619 window.addEventListener("hashchange", function (_event) {
620 self.scrollToHash();
621 });
622
623 document
624 .querySelectorAll(
625 `
626 .module-name > a,
627 .member-name a[href^='#']
628 `,
629 )
630 .forEach(function (title) {
631 title.innerHTML = title.innerHTML.replace(
632 /([A-Z])|([_/])/g,
633 "$2<wbr>$1",
634 );
635 });
636 };
637
638 /* Initialise */
639
640 init();
641
642 return self;
643})();