|
| 1 | +(function () { |
| 2 | + const sidebar = document.querySelector('#chapter-sidebar'); |
| 3 | + const toggle = document.querySelector('.sidebar-toggle'); |
| 4 | + |
| 5 | + if (sidebar && toggle) { |
| 6 | + toggle.addEventListener('click', () => { |
| 7 | + const isOpen = sidebar.classList.toggle('is-open'); |
| 8 | + toggle.setAttribute('aria-expanded', String(isOpen)); |
| 9 | + }); |
| 10 | + |
| 11 | + sidebar.addEventListener('click', (event) => { |
| 12 | + if (event.target.closest('a') && sidebar.classList.contains('is-open')) { |
| 13 | + sidebar.classList.remove('is-open'); |
| 14 | + toggle.setAttribute('aria-expanded', 'false'); |
| 15 | + } |
| 16 | + }); |
| 17 | + } |
| 18 | + |
| 19 | + // Mark code figures that require a live run / secret with an accessible badge. |
| 20 | + document.querySelectorAll('figure.code.needs-secret').forEach((figure) => { |
| 21 | + if (figure.querySelector('.code-badge')) return; |
| 22 | + const badge = document.createElement('span'); |
| 23 | + badge.className = 'code-badge'; |
| 24 | + badge.textContent = '🔒 Requires a secret / live run'; |
| 25 | + const caption = figure.querySelector(':scope > figcaption'); |
| 26 | + if (caption) { |
| 27 | + caption.insertBefore(badge, caption.firstChild); |
| 28 | + } else { |
| 29 | + figure.insertBefore(badge, figure.firstChild); |
| 30 | + } |
| 31 | + }); |
| 32 | + |
| 33 | + // Add a copy button to every code block, hosted by its figure or a wrapper div. |
| 34 | + document.querySelectorAll('pre > code').forEach((codeBlock) => { |
| 35 | + const pre = codeBlock.parentElement; |
| 36 | + if (!pre) return; |
| 37 | + |
| 38 | + const figure = pre.closest('figure.code'); |
| 39 | + let host; |
| 40 | + if (figure) { |
| 41 | + host = figure; |
| 42 | + } else if (pre.parentElement.classList.contains('code-block')) { |
| 43 | + host = pre.parentElement; |
| 44 | + } else { |
| 45 | + host = document.createElement('div'); |
| 46 | + host.className = 'code-block'; |
| 47 | + pre.parentNode.insertBefore(host, pre); |
| 48 | + host.appendChild(pre); |
| 49 | + } |
| 50 | + if (host.querySelector(':scope > .copy-code')) return; |
| 51 | + |
| 52 | + const button = document.createElement('button'); |
| 53 | + button.type = 'button'; |
| 54 | + button.className = 'copy-code'; |
| 55 | + button.textContent = 'Copy'; |
| 56 | + button.setAttribute('aria-label', 'Copy code to clipboard'); |
| 57 | + host.appendChild(button); |
| 58 | + |
| 59 | + button.addEventListener('click', async () => { |
| 60 | + try { |
| 61 | + await navigator.clipboard.writeText(codeBlock.textContent); |
| 62 | + button.textContent = 'Copied'; |
| 63 | + window.setTimeout(() => { button.textContent = 'Copy'; }, 1600); |
| 64 | + } catch (_error) { |
| 65 | + button.textContent = 'Copy failed'; |
| 66 | + window.setTimeout(() => { button.textContent = 'Copy'; }, 1600); |
| 67 | + } |
| 68 | + }); |
| 69 | + }); |
| 70 | + |
| 71 | + document.querySelectorAll('a[href^="#"]').forEach((link) => { |
| 72 | + link.addEventListener('click', (event) => { |
| 73 | + const id = link.getAttribute('href').slice(1); |
| 74 | + if (!id) return; |
| 75 | + const target = document.getElementById(decodeURIComponent(id)); |
| 76 | + if (!target) return; |
| 77 | + event.preventDefault(); |
| 78 | + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
| 79 | + history.pushState(null, '', `#${id}`); |
| 80 | + target.setAttribute('tabindex', '-1'); |
| 81 | + target.focus({ preventScroll: true }); |
| 82 | + }); |
| 83 | + }); |
| 84 | + |
| 85 | + // Reading theme: light / sepia / dark, remembered across visits. A no-flash |
| 86 | + // inline <script> in the <head> applies the stored theme before first paint; |
| 87 | + // here we wire the segmented control and keep it in sync. |
| 88 | + (function theme() { |
| 89 | + const KEY = 'aw-theme'; |
| 90 | + const root = document.documentElement; |
| 91 | + const opts = Array.from(document.querySelectorAll('.theme-opt')); |
| 92 | + if (!opts.length) return; |
| 93 | + |
| 94 | + const mql = window.matchMedia('(prefers-color-scheme: dark)'); |
| 95 | + const stored = () => { try { return localStorage.getItem(KEY); } catch (_e) { return null; } }; |
| 96 | + const effective = () => stored() || (mql.matches ? 'dark' : 'light'); |
| 97 | + |
| 98 | + const sync = () => { |
| 99 | + const active = effective(); |
| 100 | + opts.forEach((btn) => { |
| 101 | + btn.setAttribute('aria-pressed', String(btn.dataset.themeValue === active)); |
| 102 | + }); |
| 103 | + }; |
| 104 | + |
| 105 | + opts.forEach((btn) => { |
| 106 | + btn.addEventListener('click', () => { |
| 107 | + const value = btn.dataset.themeValue; |
| 108 | + root.setAttribute('data-theme', value); |
| 109 | + try { localStorage.setItem(KEY, value); } catch (_e) { /* ignore */ } |
| 110 | + sync(); |
| 111 | + }); |
| 112 | + }); |
| 113 | + |
| 114 | + // When the reader is on "system" (nothing stored) and the OS flips, follow it. |
| 115 | + mql.addEventListener('change', () => { if (!stored()) sync(); }); |
| 116 | + sync(); |
| 117 | + })(); |
| 118 | + |
| 119 | + if (window.hljs) { |
| 120 | + window.hljs.configure({ languages: ['yaml', 'yml', 'bash', 'shell', 'json', 'markdown', 'python', 'javascript', 'http'] }); |
| 121 | + window.hljs.highlightAll(); |
| 122 | + } |
| 123 | +})(); |
0 commit comments