Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

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})();