Testing of the @doc-json output
0
fork

Configure Feed

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

at main 579 lines 19 kB view raw
1let js = {| 2// Global state 3const BASE_URL = window.BASE_URL || './'; 4let CURRENT_URL = window.CURRENT_URL || 'index.html'; 5let sidebarData = null; 6 7// Compute the root URL for absolute fetching (handles SPA navigation) 8const ROOT_URL = new URL(BASE_URL, window.location.href).href; 9 10// Sidebar state management 11const STORAGE_KEY = 'odoc-docsite-sidebar-state'; 12let sidebarState = {}; 13try { 14 sidebarState = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); 15} catch (e) {} 16 17function saveSidebarState() { 18 try { 19 localStorage.setItem(STORAGE_KEY, JSON.stringify(sidebarState)); 20 } catch (e) {} 21} 22 23function escapeHtml(s) { 24 return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') 25 .replace(/"/g, '&quot;').replace(/'/g, '&#39;'); 26} 27 28// Sidebar rendering 29function kindBadge(kind) { 30 if (!kind) return ''; 31 const labels = { 32 'package': 'P', 'module': 'M', 'module-type': 'MT', 33 'page': 'pg', 'leaf-page': 'pg', 'class': 'C', 'class-type': 'CT', 34 'library': 'L' 35 }; 36 const label = labels[kind] || kind[0].toUpperCase(); 37 var cssKind = kind.replace(/[^a-z]/g, '-'); 38 return '<span class="sidebar-kind sidebar-kind-' + cssKind + '" title="' + kind + '">' + label + '</span>'; 39} 40 41function renderSidebarEntry(entry) { 42 var node = entry.node; 43 var hasChildren = entry.children && entry.children.length > 0; 44 var isActive = node.url === CURRENT_URL; 45 var stateKey = node.content; 46 // Default to collapsed unless explicitly expanded 47 var isCollapsed = sidebarState[stateKey] !== true; 48 var badge = kindBadge(node.kind); 49 50 // Content may contain inline HTML (e.g. <code>odoc</code>), so we 51 // pass it through as-is rather than escaping it. 52 var contentHtml = node.content; 53 54 // Render packages/modules with children as collapsible groups 55 if (hasChildren) { 56 var childrenHtml = entry.children.map(function(c) { return renderSidebarEntry(c); }).join(''); 57 var linkHtml = node.url 58 ? '<a class="sidebar-link' + (isActive ? ' active' : '') + '" href="' + BASE_URL + node.url + '" data-nav="' + node.url + '">' + badge + contentHtml + '</a>' 59 : '<span class="sidebar-label">' + badge + contentHtml + '</span>'; 60 return '<div class="sidebar-group' + (isCollapsed ? ' collapsed' : '') + '" data-id="' + escapeHtml(stateKey) + '">' + 61 '<div class="sidebar-group-header">' + 62 '<svg class="sidebar-toggle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' + 63 '<polyline points="6 9 12 15 18 9"></polyline>' + 64 '</svg>' + 65 linkHtml + 66 '</div>' + 67 '<div class="sidebar-children">' + childrenHtml + '</div>' + 68 '</div>'; 69 } else if (node.url) { 70 // Leaf nodes: add spacer for alignment with expandable items 71 var spacer = '<span class="sidebar-toggle-spacer"></span>'; 72 return '<a class="sidebar-link sidebar-leaf' + (isActive ? ' active' : '') + '" href="' + BASE_URL + node.url + '" data-nav="' + node.url + '">' + spacer + badge + contentHtml + '</a>'; 73 } else { 74 return '<span class="sidebar-link">' + badge + contentHtml + '</span>'; 75 } 76} 77 78function updateSidebarActive(options) { 79 options = options || {}; 80 var scrollIntoView = options.scrollIntoView || false; 81 var updatePackageFilter = options.updatePackageFilter || false; 82 var container = document.getElementById('sidebar-content'); 83 if (!container) return; 84 85 // Remove old active 86 container.querySelectorAll('.sidebar-link.active').forEach(function(el) { el.classList.remove('active'); }); 87 88 // Try to find exact match first 89 var activeLink = container.querySelector('[data-nav="' + CURRENT_URL + '"]'); 90 91 // If no exact match, find the best matching ancestor 92 if (!activeLink) { 93 var urlWithoutHash = CURRENT_URL.split('#')[0]; 94 var parts = urlWithoutHash.split('/'); 95 96 while (parts.length > 1 && !activeLink) { 97 parts.pop(); 98 var tryUrl = parts.join('/') + '/index.html'; 99 activeLink = container.querySelector('[data-nav="' + tryUrl + '"]'); 100 } 101 102 if (!activeLink) { 103 var urlPackage = getPackageFromUrl(CURRENT_URL); 104 if (urlPackage) { 105 activeLink = container.querySelector('[data-nav="' + urlPackage + '/index.html"]'); 106 } 107 } 108 } 109 110 if (activeLink) { 111 activeLink.classList.add('active'); 112 var parent = activeLink.closest('.sidebar-group'); 113 while (parent) { 114 parent.classList.remove('collapsed'); 115 sidebarState[parent.dataset.id] = true; 116 parent = parent.parentElement.closest('.sidebar-group'); 117 } 118 saveSidebarState(); 119 if (scrollIntoView) { 120 setTimeout(function() { activeLink.scrollIntoView({ block: 'center', behavior: 'instant' }); }, 0); 121 } 122 } 123 124 if (updatePackageFilter) { 125 var select = document.getElementById('package-select'); 126 if (select && sidebarData) { 127 var urlPackage2 = getPackageFromUrl(CURRENT_URL); 128 var packages = sidebarData 129 .filter(function(entry) { return entry.node.kind === 'package'; }) 130 .map(function(entry) { return entry.node.content; }); 131 132 if (CURRENT_URL === 'index.html' || !urlPackage2) { 133 if (select.value !== '') { 134 select.value = ''; 135 selectedPackage = ''; 136 filterSidebarByPackage(''); 137 } 138 } else if (packages.indexOf(urlPackage2) >= 0 && select.value !== urlPackage2) { 139 select.value = urlPackage2; 140 selectedPackage = urlPackage2; 141 filterSidebarByPackage(urlPackage2); 142 } 143 } 144 } 145} 146 147function initSidebar(data) { 148 sidebarData = data; 149 var container = document.getElementById('sidebar-content'); 150 if (!container) return; 151 152 if (CURRENT_URL === 'index.html') { 153 sidebarState = {}; 154 saveSidebarState(); 155 } 156 157 var html = data.map(function(entry) { return renderSidebarEntry(entry); }).join(''); 158 container.innerHTML = html; 159 160 container.querySelectorAll('.sidebar-group-header').forEach(function(header) { 161 header.addEventListener('click', function(e) { 162 if (e.target.closest('a')) return; 163 var group = header.closest('.sidebar-group'); 164 var id = group.dataset.id; 165 group.classList.toggle('collapsed'); 166 sidebarState[id] = !group.classList.contains('collapsed'); 167 saveSidebarState(); 168 }); 169 }); 170 171 initPackageSelector(data); 172 173 if (CURRENT_URL !== 'index.html') { 174 updateSidebarActive({ scrollIntoView: true, updatePackageFilter: true }); 175 } 176} 177 178// Package selector functionality 179var PACKAGE_STORAGE_KEY = 'odoc-docsite-selected-package'; 180var selectedPackage = ''; 181try { 182 selectedPackage = localStorage.getItem(PACKAGE_STORAGE_KEY) || ''; 183} catch (e) {} 184 185function saveSelectedPackage(pkg) { 186 selectedPackage = pkg; 187 try { 188 localStorage.setItem(PACKAGE_STORAGE_KEY, pkg); 189 } catch (e) {} 190} 191 192function getPackageFromUrl(url) { 193 var parts = url.split('/'); 194 return parts.length > 0 ? parts[0] : ''; 195} 196 197function initPackageSelector(data) { 198 var select = document.getElementById('package-select'); 199 if (!select) return; 200 201 var packages = data 202 .filter(function(entry) { return entry.node.kind === 'package'; }) 203 .map(function(entry) { return entry.node.content; }) 204 .sort(); 205 206 select.innerHTML = '<option value="">All packages</option>'; 207 packages.forEach(function(pkg) { 208 var option = document.createElement('option'); 209 option.value = pkg; 210 option.textContent = pkg; 211 select.appendChild(option); 212 }); 213 214 var urlPackage = getPackageFromUrl(CURRENT_URL); 215 if (CURRENT_URL === 'index.html' || !urlPackage) { 216 selectedPackage = ''; 217 select.value = ''; 218 filterSidebarByPackage(''); 219 } else if (packages.indexOf(urlPackage) >= 0) { 220 selectedPackage = urlPackage; 221 select.value = urlPackage; 222 filterSidebarByPackage(urlPackage); 223 } 224 225 select.addEventListener('change', function(e) { 226 var pkg = e.target.value; 227 saveSelectedPackage(pkg); 228 filterSidebarByPackage(pkg); 229 }); 230} 231 232function filterSidebarByPackage(pkg) { 233 var container = document.getElementById('sidebar-content'); 234 if (!container) return; 235 236 var groups = container.querySelectorAll(':scope > .sidebar-group, :scope > .sidebar-link'); 237 238 groups.forEach(function(group) { 239 if (!pkg) { 240 group.style.display = ''; 241 } else { 242 var id = group.dataset.id || group.textContent.trim(); 243 group.style.display = (id === pkg) ? '' : 'none'; 244 } 245 }); 246 247 if (pkg) { 248 var matchingGroup = container.querySelector('.sidebar-group[data-id="' + pkg + '"]'); 249 if (matchingGroup) { 250 matchingGroup.classList.remove('collapsed'); 251 sidebarState[pkg] = true; 252 saveSidebarState(); 253 } 254 } 255} 256 257// Highlight element matching URL hash 258function highlightHash() { 259 document.querySelectorAll('.hash-highlight').forEach(function(el) { el.classList.remove('hash-highlight'); }); 260 261 var hash = window.location.hash; 262 if (hash) { 263 var target = document.querySelector(hash); 264 if (target) { 265 target.classList.add('hash-highlight'); 266 setTimeout(function() { 267 target.scrollIntoView({ behavior: 'smooth', block: 'center' }); 268 }, 50); 269 } 270 } 271} 272 273window.addEventListener('hashchange', highlightHash); 274 275// SPA Navigation 276async function navigateTo(url, pushState) { 277 if (pushState === undefined) pushState = true; 278 try { 279 var response = await fetch(ROOT_URL + url); 280 if (!response.ok) throw new Error('Failed to load page'); 281 var html = await response.text(); 282 283 var parser = new DOMParser(); 284 var doc = parser.parseFromString(html, 'text/html'); 285 286 var newContent = doc.querySelector('.odoc-content'); 287 var newBreadcrumbs = doc.querySelector('.odoc-nav'); 288 var newToc = doc.querySelector('.odoc-tocs'); 289 var newTitle = doc.querySelector('title'); 290 291 if (newContent) { 292 document.querySelector('.odoc-content').innerHTML = newContent.innerHTML; 293 } 294 if (newBreadcrumbs) { 295 document.querySelector('.odoc-nav').innerHTML = newBreadcrumbs.innerHTML; 296 } 297 298 var existingToc = document.querySelector('.odoc-tocs'); 299 if (newToc) { 300 if (existingToc) { 301 existingToc.outerHTML = newToc.outerHTML; 302 } else { 303 document.querySelector('.docsite-main').insertAdjacentHTML('beforeend', newToc.outerHTML); 304 } 305 } else if (existingToc) { 306 existingToc.remove(); 307 } 308 309 if (newTitle) { 310 document.title = newTitle.textContent; 311 } 312 313 // Collect inline scripts first (before we start loading external scripts) 314 var fetchedPageBase = ROOT_URL + url; 315 var inlineScripts = Array.from(doc.querySelectorAll('head script:not([src])')); 316 317 // Inject external CSS/JS; track load events for newly added scripts 318 var newScriptLoadPromises = []; 319 doc.querySelectorAll('head link[rel="stylesheet"], head script[src]').forEach(function(el) { 320 var attr = el.tagName === 'LINK' ? 'href' : 'src'; 321 var resUrl = el.getAttribute(attr); 322 if (!resUrl) return; 323 var abs = new URL(resUrl, fetchedPageBase).href; 324 var selector = el.tagName === 'LINK' 325 ? 'link[rel="stylesheet"]' 326 : 'script[src]'; 327 var already = Array.from(document.querySelectorAll('head ' + selector)).some(function(existing) { 328 var existingUrl = existing.getAttribute(attr); 329 if (!existingUrl) return false; 330 return new URL(existingUrl, window.location.href).href === abs; 331 }); 332 if (!already) { 333 var clone = el.cloneNode(true); 334 clone.setAttribute(attr, abs); 335 if (el.tagName === 'SCRIPT') { 336 var p = new Promise(function(resolve) { 337 clone.onload = resolve; 338 clone.onerror = resolve; 339 }); 340 newScriptLoadPromises.push(p); 341 } 342 document.head.appendChild(clone); 343 } 344 }); 345 346 // After all newly added external scripts have loaded, execute inline scripts. 347 // Deduplicate via data-spa-inline attribute set during HTML generation. 348 Promise.all(newScriptLoadPromises).then(function() { 349 inlineScripts.forEach(function(el) { 350 var id = el.getAttribute('data-spa-inline'); 351 if (id && document.querySelector('head script[data-spa-inline="' + id + '"]')) return; 352 var s = el.cloneNode(true); 353 document.head.appendChild(s); 354 }); 355 document.dispatchEvent(new CustomEvent('odoc-spa-loaded')); 356 }); 357 358 CURRENT_URL = url; 359 if (pushState) { 360 history.pushState({ url: url }, '', ROOT_URL + url); 361 } 362 363 updateSidebarActive(); 364 365 var searchResults = document.querySelector('.search-results'); 366 var searchInput = document.querySelector('.search-input'); 367 if (searchResults) searchResults.classList.remove('active'); 368 if (searchInput) searchInput.value = ''; 369 370 var hash = url.indexOf('#') >= 0 ? '#' + url.split('#')[1] : window.location.hash; 371 if (hash) { 372 setTimeout(function() { highlightHash(); }, 100); 373 } else { 374 document.querySelector('.docsite-main').scrollTop = 0; 375 window.scrollTo(0, 0); 376 } 377 378 } catch (e) { 379 console.error('Navigation failed:', e); 380 window.location.href = ROOT_URL + url; 381 } 382} 383 384window.addEventListener('popstate', function(e) { 385 if (e.state && e.state.url) { 386 navigateTo(e.state.url, false); 387 } 388}); 389 390// Intercept link clicks for SPA navigation 391document.addEventListener('click', function(e) { 392 var link = e.target.closest('a[href]'); 393 if (!link) return; 394 395 var href = link.getAttribute('href'); 396 if (!href) return; 397 398 if (href.indexOf('http') === 0 || href.indexOf('//') === 0 || href.indexOf('#') === 0 || 399 href.indexOf('mailto:') === 0 || href.indexOf('javascript:') === 0) { 400 return; 401 } 402 403 var navPath = link.dataset.nav; 404 if (navPath) { 405 e.preventDefault(); 406 navigateTo(navPath); 407 return; 408 } 409 410 var targetUrl = href; 411 if (href.indexOf('/') === 0) { 412 targetUrl = href.slice(1); 413 } else if (href.indexOf('./') === 0) { 414 var currentDir = CURRENT_URL.substring(0, CURRENT_URL.lastIndexOf('/') + 1); 415 targetUrl = currentDir + href.slice(2); 416 } else if (href.indexOf('../') === 0) { 417 var currentParts = CURRENT_URL.split('/'); 418 currentParts.pop(); 419 var hrefParts = href.split('/'); 420 for (var i = 0; i < hrefParts.length; i++) { 421 var part = hrefParts[i]; 422 if (part === '..') { 423 currentParts.pop(); 424 } else if (part !== '.') { 425 currentParts.push(part); 426 } 427 } 428 targetUrl = currentParts.join('/'); 429 } else { 430 var curDir = CURRENT_URL.substring(0, CURRENT_URL.lastIndexOf('/') + 1); 431 targetUrl = curDir + href; 432 } 433 434 if (targetUrl.indexOf('#') >= 0) { 435 var pathAndHash = targetUrl.split('#'); 436 if (pathAndHash[0] === CURRENT_URL || pathAndHash[0] === '') { 437 return; 438 } 439 } 440 441 e.preventDefault(); 442 navigateTo(targetUrl); 443}); 444 445// Initialize 446(function() { 447 history.replaceState({ url: CURRENT_URL }, '', window.location.href); 448 449 // Read sidebar data from inline script tag if available 450 var inlineData = window.__DOCSITE_SIDEBAR_DATA__; 451 if (inlineData) { 452 initSidebar(inlineData); 453 } else { 454 // Fallback: fetch sidebar.json 455 fetch(ROOT_URL + 'sidebar.json') 456 .then(function(r) { return r.json(); }) 457 .then(function(data) { initSidebar(data); }) 458 .catch(function(e) { 459 console.error('Failed to load sidebar:', e); 460 var el = document.getElementById('sidebar-content'); 461 if (el) el.innerHTML = '<div class="sidebar-error">Failed to load navigation</div>'; 462 }); 463 } 464 465 if (window.location.hash) { 466 setTimeout(highlightHash, 100); 467 } 468})(); 469 470// Search functionality 471(function() { 472 var worker; 473 var waiting = 0; 474 475 var searchInput = document.querySelector('.search-input'); 476 var searchResults = document.querySelector('.search-results'); 477 var searchWrapper = document.querySelector('.search-wrapper'); 478 479 if (!searchInput) return; 480 481 function showBusy() { 482 waiting++; 483 searchWrapper.classList.add('search-busy'); 484 } 485 486 function hideBusy() { 487 if (waiting > 0) waiting--; 488 if (waiting === 0) { 489 searchWrapper.classList.remove('search-busy'); 490 } 491 } 492 493 function createWorker() { 494 var searchUrls = ['db.js', 'sherlodoc.js'].map(function(url) { return '"' + ROOT_URL + url + '"'; }); 495 var blob = new Blob(['importScripts(' + searchUrls.join(',') + ');'], { type: 'application/javascript' }); 496 var blobUrl = URL.createObjectURL(blob); 497 var w = new Worker(blobUrl); 498 URL.revokeObjectURL(blobUrl); 499 return w; 500 } 501 502 searchInput.addEventListener('focus', function() { 503 if (!worker) { 504 worker = createWorker(); 505 worker.onmessage = function(e) { 506 hideBusy(); 507 var results = e.data; 508 searchResults.innerHTML = ''; 509 510 if (results.length === 0 && searchInput.value.trim() !== '') { 511 searchResults.innerHTML = '<div class="search-no-result">No results found</div>'; 512 } else { 513 results.forEach(function(entry) { 514 var link = document.createElement('a'); 515 link.className = 'search-result'; 516 link.href = ROOT_URL + entry.url; 517 link.dataset.nav = entry.url; 518 link.innerHTML = entry.html; 519 searchResults.appendChild(link); 520 }); 521 } 522 523 if (searchInput.value.trim() !== '') { 524 searchResults.classList.add('active'); 525 } 526 }; 527 } 528 }); 529 530 searchInput.addEventListener('input', function(e) { 531 var query = e.target.value.trim(); 532 if (query === '') { 533 searchResults.classList.remove('active'); 534 searchResults.innerHTML = ''; 535 return; 536 } 537 showBusy(); 538 worker.postMessage(query); 539 }); 540 541 document.addEventListener('keydown', function(e) { 542 if (e.key === '/' && document.activeElement !== searchInput) { 543 e.preventDefault(); 544 searchInput.focus(); 545 } 546 if (e.key === 'Escape' && document.activeElement === searchInput) { 547 searchInput.blur(); 548 searchResults.classList.remove('active'); 549 } 550 if (e.key === 'ArrowDown' && searchResults.classList.contains('active')) { 551 e.preventDefault(); 552 var items = Array.from(searchResults.querySelectorAll('.search-result')); 553 var idx = items.findIndex(function(el) { return el === document.activeElement; }); 554 if (idx < items.length - 1) items[idx + 1].focus(); 555 else if (idx === -1 && items.length > 0) items[0].focus(); 556 } 557 if (e.key === 'ArrowUp' && searchResults.classList.contains('active')) { 558 e.preventDefault(); 559 var items2 = Array.from(searchResults.querySelectorAll('.search-result')); 560 var idx2 = items2.findIndex(function(el) { return el === document.activeElement; }); 561 if (idx2 > 0) items2[idx2 - 1].focus(); 562 else if (idx2 === 0) searchInput.focus(); 563 } 564 }); 565 566 document.addEventListener('click', function(e) { 567 if (!searchWrapper.contains(e.target)) searchResults.classList.remove('active'); 568 }); 569})(); 570 571// Mobile menu toggle 572(function() { 573 var menuToggle = document.querySelector('.menu-toggle'); 574 var sidebar = document.querySelector('.docsite-sidebar'); 575 if (menuToggle && sidebar) { 576 menuToggle.addEventListener('click', function() { sidebar.classList.toggle('open'); }); 577 } 578})(); 579|}