book.js
1 "use strict"; 2 3 // Fix back button cache problem 4 window.onunload = function () { }; 5 6 // Global variable, shared between modules 7 function playpen_text(playpen) { 8 let code_block = playpen.querySelector("code"); 9 10 if (window.ace && code_block.classList.contains("editable")) { 11 let editor = window.ace.edit(code_block); 12 return editor.getValue(); 13 } else { 14 return code_block.textContent; 15 } 16 } 17 18 (function codeSnippets() { 19 function fetch_with_timeout(url, options, timeout = 6000) { 20 return Promise.race([ 21 fetch(url, options), 22 new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)) 23 ]); 24 } 25 26 var playpens = Array.from(document.querySelectorAll(".playpen")); 27 if (playpens.length > 0) { 28 fetch_with_timeout("https://play.rust-lang.org/meta/crates", { 29 headers: { 30 'Content-Type': "application/json", 31 }, 32 method: 'POST', 33 mode: 'cors', 34 }) 35 .then(response => response.json()) 36 .then(response => { 37 // get list of crates available in the rust playground 38 let playground_crates = response.crates.map(item => item["id"]); 39 playpens.forEach(block => handle_crate_list_update(block, playground_crates)); 40 }); 41 } 42 43 function handle_crate_list_update(playpen_block, playground_crates) { 44 // update the play buttons after receiving the response 45 update_play_button(playpen_block, playground_crates); 46 47 // and install on change listener to dynamically update ACE editors 48 if (window.ace) { 49 let code_block = playpen_block.querySelector("code"); 50 if (code_block.classList.contains("editable")) { 51 let editor = window.ace.edit(code_block); 52 editor.addEventListener("change", function (e) { 53 update_play_button(playpen_block, playground_crates); 54 }); 55 // add Ctrl-Enter command to execute rust code 56 editor.commands.addCommand({ 57 name: "run", 58 bindKey: { 59 win: "Ctrl-Enter", 60 mac: "Ctrl-Enter" 61 }, 62 exec: _editor => run_rust_code(playpen_block) 63 }); 64 } 65 } 66 } 67 68 // updates the visibility of play button based on `no_run` class and 69 // used crates vs ones available on http://play.rust-lang.org 70 function update_play_button(pre_block, playground_crates) { 71 var play_button = pre_block.querySelector(".play-button"); 72 73 // skip if code is `no_run` 74 if (pre_block.querySelector('code').classList.contains("no_run")) { 75 play_button.classList.add("hidden"); 76 return; 77 } 78 79 // get list of `extern crate`'s from snippet 80 var txt = playpen_text(pre_block); 81 var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g; 82 var snippet_crates = []; 83 var item; 84 while (item = re.exec(txt)) { 85 snippet_crates.push(item[1]); 86 } 87 88 // check if all used crates are available on play.rust-lang.org 89 var all_available = snippet_crates.every(function (elem) { 90 return playground_crates.indexOf(elem) > -1; 91 }); 92 93 if (all_available) { 94 play_button.classList.remove("hidden"); 95 } else { 96 play_button.classList.add("hidden"); 97 } 98 } 99 100 function run_rust_code(code_block) { 101 var result_block = code_block.querySelector(".result"); 102 if (!result_block) { 103 result_block = document.createElement('code'); 104 result_block.className = 'result hljs language-bash'; 105 106 code_block.append(result_block); 107 } 108 109 let text = playpen_text(code_block); 110 let classes = code_block.querySelector('code').classList; 111 let has_2018 = classes.contains("edition2018"); 112 let edition = has_2018 ? "2018" : "2015"; 113 114 var params = { 115 version: "stable", 116 optimize: "0", 117 code: text, 118 edition: edition 119 }; 120 121 if (text.indexOf("#![feature") !== -1) { 122 params.version = "nightly"; 123 } 124 125 result_block.innerText = "Running..."; 126 127 fetch_with_timeout("https://play.rust-lang.org/evaluate.json", { 128 headers: { 129 'Content-Type': "application/json", 130 }, 131 method: 'POST', 132 mode: 'cors', 133 body: JSON.stringify(params) 134 }) 135 .then(response => response.json()) 136 .then(response => result_block.innerText = response.result) 137 .catch(error => result_block.innerText = "Playground Communication: " + error.message); 138 } 139 140 // Syntax highlighting Configuration 141 hljs.configure({ 142 tabReplace: ' ', // 4 spaces 143 languages: [], // Languages used for auto-detection 144 }); 145 146 let code_nodes = Array 147 .from(document.querySelectorAll('code')) 148 // Don't highlight `inline code` blocks in headers. 149 .filter(function (node) {return !node.parentElement.classList.contains("header"); }); 150 151 if (window.ace) { 152 // language-rust class needs to be removed for editable 153 // blocks or highlightjs will capture events 154 Array 155 .from(document.querySelectorAll('code.editable')) 156 .forEach(function (block) { block.classList.remove('language-rust'); }); 157 158 Array 159 .from(document.querySelectorAll('code:not(.editable)')) 160 .forEach(function (block) { hljs.highlightBlock(block); }); 161 } else { 162 code_nodes.forEach(function (block) { hljs.highlightBlock(block); }); 163 } 164 165 // Adding the hljs class gives code blocks the color css 166 // even if highlighting doesn't apply 167 code_nodes.forEach(function (block) { block.classList.add('hljs'); }); 168 169 Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) { 170 171 var lines = Array.from(block.querySelectorAll('.boring')); 172 // If no lines were hidden, return 173 if (!lines.length) { return; } 174 block.classList.add("hide-boring"); 175 176 var buttons = document.createElement('div'); 177 buttons.className = 'buttons'; 178 buttons.innerHTML = "<button class=\"fa fa-expand\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>"; 179 180 // add expand button 181 var pre_block = block.parentNode; 182 pre_block.insertBefore(buttons, pre_block.firstChild); 183 184 pre_block.querySelector('.buttons').addEventListener('click', function (e) { 185 if (e.target.classList.contains('fa-expand')) { 186 e.target.classList.remove('fa-expand'); 187 e.target.classList.add('fa-compress'); 188 e.target.title = 'Hide lines'; 189 e.target.setAttribute('aria-label', e.target.title); 190 191 block.classList.remove('hide-boring'); 192 } else if (e.target.classList.contains('fa-compress')) { 193 e.target.classList.remove('fa-compress'); 194 e.target.classList.add('fa-expand'); 195 e.target.title = 'Show hidden lines'; 196 e.target.setAttribute('aria-label', e.target.title); 197 198 block.classList.add('hide-boring'); 199 } 200 }); 201 }); 202 203 if (window.playpen_copyable) { 204 Array.from(document.querySelectorAll('pre code')).forEach(function (block) { 205 var pre_block = block.parentNode; 206 if (!pre_block.classList.contains('playpen')) { 207 var buttons = pre_block.querySelector(".buttons"); 208 if (!buttons) { 209 buttons = document.createElement('div'); 210 buttons.className = 'buttons'; 211 pre_block.insertBefore(buttons, pre_block.firstChild); 212 } 213 214 var clipButton = document.createElement('button'); 215 clipButton.className = 'fa fa-copy clip-button'; 216 clipButton.title = 'Copy to clipboard'; 217 clipButton.setAttribute('aria-label', clipButton.title); 218 clipButton.innerHTML = '<i class=\"tooltiptext\"></i>'; 219 220 buttons.insertBefore(clipButton, buttons.firstChild); 221 } 222 }); 223 } 224 225 // Process playpen code blocks 226 Array.from(document.querySelectorAll(".playpen")).forEach(function (pre_block) { 227 // Add play button 228 var buttons = pre_block.querySelector(".buttons"); 229 if (!buttons) { 230 buttons = document.createElement('div'); 231 buttons.className = 'buttons'; 232 pre_block.insertBefore(buttons, pre_block.firstChild); 233 } 234 235 var runCodeButton = document.createElement('button'); 236 runCodeButton.className = 'fa fa-play play-button'; 237 runCodeButton.hidden = true; 238 runCodeButton.title = 'Run this code'; 239 runCodeButton.setAttribute('aria-label', runCodeButton.title); 240 241 buttons.insertBefore(runCodeButton, buttons.firstChild); 242 runCodeButton.addEventListener('click', function (e) { 243 run_rust_code(pre_block); 244 }); 245 246 if (window.playpen_copyable) { 247 var copyCodeClipboardButton = document.createElement('button'); 248 copyCodeClipboardButton.className = 'fa fa-copy clip-button'; 249 copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>'; 250 copyCodeClipboardButton.title = 'Copy to clipboard'; 251 copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title); 252 253 buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild); 254 } 255 256 let code_block = pre_block.querySelector("code"); 257 if (window.ace && code_block.classList.contains("editable")) { 258 var undoChangesButton = document.createElement('button'); 259 undoChangesButton.className = 'fa fa-history reset-button'; 260 undoChangesButton.title = 'Undo changes'; 261 undoChangesButton.setAttribute('aria-label', undoChangesButton.title); 262 263 buttons.insertBefore(undoChangesButton, buttons.firstChild); 264 265 undoChangesButton.addEventListener('click', function () { 266 let editor = window.ace.edit(code_block); 267 editor.setValue(editor.originalCode); 268 editor.clearSelection(); 269 }); 270 } 271 }); 272 })(); 273 274 (function themes() { 275 var html = document.querySelector('html'); 276 var themeToggleButton = document.getElementById('theme-toggle'); 277 var themePopup = document.getElementById('theme-list'); 278 var themeColorMetaTag = document.querySelector('meta[name="theme-color"]'); 279 var stylesheets = { 280 ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"), 281 tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"), 282 highlight: document.querySelector("[href$='highlight.css']"), 283 }; 284 285 function showThemes() { 286 themePopup.style.display = 'block'; 287 themeToggleButton.setAttribute('aria-expanded', true); 288 themePopup.querySelector("button#" + get_theme()).focus(); 289 } 290 291 function hideThemes() { 292 themePopup.style.display = 'none'; 293 themeToggleButton.setAttribute('aria-expanded', false); 294 themeToggleButton.focus(); 295 } 296 297 function get_theme() { 298 var theme; 299 try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { } 300 if (theme === null || theme === undefined) { 301 return default_theme; 302 } else { 303 return theme; 304 } 305 } 306 307 function set_theme(theme, store = true) { 308 let ace_theme; 309 310 if (theme == 'coal' || theme == 'navy') { 311 stylesheets.ayuHighlight.disabled = true; 312 stylesheets.tomorrowNight.disabled = false; 313 stylesheets.highlight.disabled = true; 314 315 ace_theme = "ace/theme/tomorrow_night"; 316 } else if (theme == 'ayu') { 317 stylesheets.ayuHighlight.disabled = false; 318 stylesheets.tomorrowNight.disabled = true; 319 stylesheets.highlight.disabled = true; 320 ace_theme = "ace/theme/tomorrow_night"; 321 } else { 322 stylesheets.ayuHighlight.disabled = true; 323 stylesheets.tomorrowNight.disabled = true; 324 stylesheets.highlight.disabled = false; 325 ace_theme = "ace/theme/dawn"; 326 } 327 328 setTimeout(function () { 329 themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor; 330 }, 1); 331 332 if (window.ace && window.editors) { 333 window.editors.forEach(function (editor) { 334 editor.setTheme(ace_theme); 335 }); 336 } 337 338 var previousTheme = get_theme(); 339 340 if (store) { 341 try { localStorage.setItem('mdbook-theme', theme); } catch (e) { } 342 } 343 344 html.classList.remove(previousTheme); 345 html.classList.add(theme); 346 } 347 348 // Set theme 349 var theme = get_theme(); 350 351 set_theme(theme, false); 352 353 themeToggleButton.addEventListener('click', function () { 354 if (themePopup.style.display === 'block') { 355 hideThemes(); 356 } else { 357 showThemes(); 358 } 359 }); 360 361 themePopup.addEventListener('click', function (e) { 362 var theme = e.target.id || e.target.parentElement.id; 363 set_theme(theme); 364 }); 365 366 themePopup.addEventListener('focusout', function(e) { 367 // e.relatedTarget is null in Safari and Firefox on macOS (see workaround below) 368 if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) { 369 hideThemes(); 370 } 371 }); 372 373 // Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628 374 document.addEventListener('click', function(e) { 375 if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) { 376 hideThemes(); 377 } 378 }); 379 380 document.addEventListener('keydown', function (e) { 381 if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } 382 if (!themePopup.contains(e.target)) { return; } 383 384 switch (e.key) { 385 case 'Escape': 386 e.preventDefault(); 387 hideThemes(); 388 break; 389 case 'ArrowUp': 390 e.preventDefault(); 391 var li = document.activeElement.parentElement; 392 if (li && li.previousElementSibling) { 393 li.previousElementSibling.querySelector('button').focus(); 394 } 395 break; 396 case 'ArrowDown': 397 e.preventDefault(); 398 var li = document.activeElement.parentElement; 399 if (li && li.nextElementSibling) { 400 li.nextElementSibling.querySelector('button').focus(); 401 } 402 break; 403 case 'Home': 404 e.preventDefault(); 405 themePopup.querySelector('li:first-child button').focus(); 406 break; 407 case 'End': 408 e.preventDefault(); 409 themePopup.querySelector('li:last-child button').focus(); 410 break; 411 } 412 }); 413 })(); 414 415 (function sidebar() { 416 var html = document.querySelector("html"); 417 var sidebar = document.getElementById("sidebar"); 418 var sidebarLinks = document.querySelectorAll('#sidebar a'); 419 var sidebarToggleButton = document.getElementById("sidebar-toggle"); 420 var sidebarResizeHandle = document.getElementById("sidebar-resize-handle"); 421 var firstContact = null; 422 423 function showSidebar() { 424 html.classList.remove('sidebar-hidden') 425 html.classList.add('sidebar-visible'); 426 Array.from(sidebarLinks).forEach(function (link) { 427 link.setAttribute('tabIndex', 0); 428 }); 429 sidebarToggleButton.setAttribute('aria-expanded', true); 430 sidebar.setAttribute('aria-hidden', false); 431 try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { } 432 } 433 434 435 var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle'); 436 437 function toggleSection(ev) { 438 ev.currentTarget.parentElement.classList.toggle('expanded'); 439 } 440 441 Array.from(sidebarAnchorToggles).forEach(function (el) { 442 el.addEventListener('click', toggleSection); 443 }); 444 445 function hideSidebar() { 446 html.classList.remove('sidebar-visible') 447 html.classList.add('sidebar-hidden'); 448 Array.from(sidebarLinks).forEach(function (link) { 449 link.setAttribute('tabIndex', -1); 450 }); 451 sidebarToggleButton.setAttribute('aria-expanded', false); 452 sidebar.setAttribute('aria-hidden', true); 453 try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { } 454 } 455 456 // Toggle sidebar 457 sidebarToggleButton.addEventListener('click', function sidebarToggle() { 458 if (html.classList.contains("sidebar-hidden")) { 459 showSidebar(); 460 } else if (html.classList.contains("sidebar-visible")) { 461 hideSidebar(); 462 } else { 463 if (getComputedStyle(sidebar)['transform'] === 'none') { 464 hideSidebar(); 465 } else { 466 showSidebar(); 467 } 468 } 469 }); 470 471 sidebarResizeHandle.addEventListener('mousedown', initResize, false); 472 473 function initResize(e) { 474 window.addEventListener('mousemove', resize, false); 475 window.addEventListener('mouseup', stopResize, false); 476 html.classList.add('sidebar-resizing'); 477 } 478 function resize(e) { 479 document.documentElement.style.setProperty('--sidebar-width', (e.clientX - sidebar.offsetLeft) + 'px'); 480 } 481 //on mouseup remove windows functions mousemove & mouseup 482 function stopResize(e) { 483 html.classList.remove('sidebar-resizing'); 484 window.removeEventListener('mousemove', resize, false); 485 window.removeEventListener('mouseup', stopResize, false); 486 } 487 488 document.addEventListener('touchstart', function (e) { 489 firstContact = { 490 x: e.touches[0].clientX, 491 time: Date.now() 492 }; 493 }, { passive: true }); 494 495 document.addEventListener('touchmove', function (e) { 496 if (!firstContact) 497 return; 498 499 var curX = e.touches[0].clientX; 500 var xDiff = curX - firstContact.x, 501 tDiff = Date.now() - firstContact.time; 502 503 if (tDiff < 250 && Math.abs(xDiff) >= 150) { 504 if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300)) 505 showSidebar(); 506 else if (xDiff < 0 && curX < 300) 507 hideSidebar(); 508 509 firstContact = null; 510 } 511 }, { passive: true }); 512 513 // Scroll sidebar to current active section 514 var activeSection = document.getElementById("sidebar").querySelector(".active"); 515 if (activeSection) { 516 // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView 517 activeSection.scrollIntoView({ block: 'center' }); 518 } 519 })(); 520 521 (function chapterNavigation() { 522 document.addEventListener('keydown', function (e) { 523 if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } 524 if (window.search && window.search.hasFocus()) { return; } 525 526 switch (e.key) { 527 case 'ArrowRight': 528 e.preventDefault(); 529 var nextButton = document.querySelector('.nav-chapters.next'); 530 if (nextButton) { 531 window.location.href = nextButton.href; 532 } 533 break; 534 case 'ArrowLeft': 535 e.preventDefault(); 536 var previousButton = document.querySelector('.nav-chapters.previous'); 537 if (previousButton) { 538 window.location.href = previousButton.href; 539 } 540 break; 541 } 542 }); 543 })(); 544 545 (function clipboard() { 546 var clipButtons = document.querySelectorAll('.clip-button'); 547 548 function hideTooltip(elem) { 549 elem.firstChild.innerText = ""; 550 elem.className = 'fa fa-copy clip-button'; 551 } 552 553 function showTooltip(elem, msg) { 554 elem.firstChild.innerText = msg; 555 elem.className = 'fa fa-copy tooltipped'; 556 } 557 558 var clipboardSnippets = new ClipboardJS('.clip-button', { 559 text: function (trigger) { 560 hideTooltip(trigger); 561 let playpen = trigger.closest("pre"); 562 return playpen_text(playpen); 563 } 564 }); 565 566 Array.from(clipButtons).forEach(function (clipButton) { 567 clipButton.addEventListener('mouseout', function (e) { 568 hideTooltip(e.currentTarget); 569 }); 570 }); 571 572 clipboardSnippets.on('success', function (e) { 573 e.clearSelection(); 574 showTooltip(e.trigger, "Copied!"); 575 }); 576 577 clipboardSnippets.on('error', function (e) { 578 showTooltip(e.trigger, "Clipboard error!"); 579 }); 580 })(); 581 582 (function scrollToTop () { 583 var menuTitle = document.querySelector('.menu-title'); 584 585 menuTitle.addEventListener('click', function () { 586 document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' }); 587 }); 588 })(); 589 590 (function controllMenu() { 591 var menu = document.getElementById('menu-bar'); 592 593 (function controllPosition() { 594 var scrollTop = document.scrollingElement.scrollTop; 595 var prevScrollTop = scrollTop; 596 var minMenuY = -menu.clientHeight - 50; 597 // When the script loads, the page can be at any scroll (e.g. if you reforesh it). 598 menu.style.top = scrollTop + 'px'; 599 // Same as parseInt(menu.style.top.slice(0, -2), but faster 600 var topCache = menu.style.top.slice(0, -2); 601 menu.classList.remove('sticky'); 602 var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster 603 document.addEventListener('scroll', function () { 604 scrollTop = Math.max(document.scrollingElement.scrollTop, 0); 605 // `null` means that it doesn't need to be updated 606 var nextSticky = null; 607 var nextTop = null; 608 var scrollDown = scrollTop > prevScrollTop; 609 var menuPosAbsoluteY = topCache - scrollTop; 610 if (scrollDown) { 611 nextSticky = false; 612 if (menuPosAbsoluteY > 0) { 613 nextTop = prevScrollTop; 614 } 615 } else { 616 if (menuPosAbsoluteY > 0) { 617 nextSticky = true; 618 } else if (menuPosAbsoluteY < minMenuY) { 619 nextTop = prevScrollTop + minMenuY; 620 } 621 } 622 if (nextSticky === true && stickyCache === false) { 623 menu.classList.add('sticky'); 624 stickyCache = true; 625 } else if (nextSticky === false && stickyCache === true) { 626 menu.classList.remove('sticky'); 627 stickyCache = false; 628 } 629 if (nextTop !== null) { 630 menu.style.top = nextTop + 'px'; 631 topCache = nextTop; 632 } 633 prevScrollTop = scrollTop; 634 }, { passive: true }); 635 })(); 636 (function controllBorder() { 637 menu.classList.remove('bordered'); 638 document.addEventListener('scroll', function () { 639 if (menu.offsetTop === 0) { 640 menu.classList.remove('bordered'); 641 } else { 642 menu.classList.add('bordered'); 643 } 644 }, { passive: true }); 645 })(); 646 })();