function enableThemeToggle() { const themeToggle = document.querySelector('#theme-toggle'); if (!themeToggle) return; const hlLink = document.querySelector('link#hl'); const preferDark = window.matchMedia("(prefers-color-scheme: dark)"); function toggleTheme(theme) { if (theme == "dark") document.body.classList.add('dark'); else document.body.classList.remove('dark'); if (hlLink) hlLink.href = `/hl-${theme}.css`; sessionStorage.setItem("theme", theme); toggleGiscusTheme(theme); } function toggleGiscusTheme(theme) { const iframe = document.querySelector('iframe.giscus-frame'); if (iframe) iframe.contentWindow.postMessage({ giscus: { setConfig: { theme: `${location.origin}/giscus_${theme}.css` } } }, 'https://giscus.app'); } function initGiscusTheme(evt) { if (evt.origin !== 'https://giscus.app') return; if (!(typeof evt.data === 'object' && evt.data.giscus)) return; toggleGiscusTheme(sessionStorage.getItem("theme") || (preferDark.matches ? "dark" : "light")); window.removeEventListener('message', initGiscusTheme); } window.addEventListener('message', initGiscusTheme); themeToggle.addEventListener('click', () => toggleTheme(sessionStorage.getItem("theme") == "dark" ? "light" : "dark")); preferDark.addEventListener("change", e => toggleTheme(e.matches ? "dark" : "light")); if (!sessionStorage.getItem("theme") && preferDark.matches) toggleTheme("dark"); if (sessionStorage.getItem("theme") == "dark") toggleTheme("dark"); } function enablePrerender() { const prerender = (a) => { if (!a.classList.contains('instant')) return; const script = document.createElement('script'); script.type = 'speculationrules'; script.textContent = JSON.stringify({ prerender: [{ source: 'list', urls: [a.href] }] }); document.body.append(script); a.classList.remove('instant'); } const prefetch = (a) => { if (!a.classList.contains('instant')) return; const link = document.createElement('link'); link.rel = 'prefetch'; link.href = a.href; document.head.append(link); a.classList.remove('instant'); } const support = HTMLScriptElement.supports && HTMLScriptElement.supports('speculationrules'); const handle = support ? prerender : prefetch; document.querySelectorAll('a.instant').forEach(a => { if (a.href.endsWith(window.location.pathname)) return; let timer; a.addEventListener('mouseenter', () => { timer = setTimeout(() => handle(a), 50); }); a.addEventListener('mouseleave', () => clearTimeout(timer)); a.addEventListener('touchstart', () => handle(a), { passive: true }); }); } function enableRssMask() { const rssBtn = document.querySelector('#rss-btn'); const mask = document.querySelector('#rss-mask'); const copyBtn = document.querySelector('#rss-mask button'); if (!rssBtn || !mask) return; rssBtn.addEventListener('click', (e) => { e.preventDefault(); mask.showModal(); }); const close = (e) => { if (e.target == mask) mask.close(); }; mask.addEventListener('click', close); const copy = () => { navigator.clipboard.writeText(copyBtn.dataset.link).then(() => { copyBtn.innerHTML = copyBtn.dataset.checkIcon; copyBtn.classList.add('copied'); copyBtn.removeEventListener('click', copy); setTimeout(() => { mask.close(); copyBtn.innerHTML = copyBtn.dataset.copyIcon; copyBtn.classList.remove('copied'); copyBtn.addEventListener('click', copy); }, 400); }); } copyBtn.addEventListener('click', copy); } function enableOutdateAlert() { const alert = document.querySelector('#outdate_alert'); if (!alert) return; const publish = document.querySelector('#publish'); const updated = document.querySelector('#updated'); const updateDate = new Date(updated ? updated.textContent : publish.textContent); const intervalDays = Math.floor((Date.now() - updateDate.getTime()) / (24 * 60 * 60 * 1000)); const alertDays = parseInt(alert.dataset.days); if (intervalDays >= alertDays) { const msg = alert.dataset.alertTextBefore + intervalDays + alert.dataset.alertTextAfter; alert.querySelector('.content').textContent = msg; alert.classList.remove('hidden'); } } function enableTocTooltip() { const anchors = document.querySelectorAll('aside nav a'); if (anchors.length == 0) return; const toggleTooltip = () => { anchors.forEach(anchor => { if (anchor.offsetWidth < anchor.scrollWidth) { anchor.setAttribute('title', anchor.textContent); } else { anchor.removeAttribute('title'); } }); }; window.addEventListener('resize', toggleTooltip); toggleTooltip(); } function addCopyBtns() { const cfg = document.querySelector('#copy-cfg'); if (!cfg) return; const copyIcon = cfg.dataset.copyIcon; const checkIcon = cfg.dataset.checkIcon; document.querySelectorAll('pre').forEach(block => { if (block.classList.contains('mermaid')) return; const wrapper = document.createElement('div'); wrapper.className = 'codeblock'; const btn = document.createElement('button'); btn.className = 'copy'; btn.ariaLabel = 'copy'; btn.innerHTML = copyIcon; const copy = () => { navigator.clipboard.writeText(block.textContent).then(() => { btn.innerHTML = checkIcon; btn.classList.add('copied'); btn.removeEventListener('click', copy); setTimeout(() => { btn.innerHTML = copyIcon; btn.classList.remove('copied'); btn.addEventListener('click', copy); }, 1500); }); }; btn.addEventListener('click', copy); wrapper.appendChild(block.cloneNode(true)); wrapper.appendChild(btn); block.replaceWith(wrapper); }); } function addBackToTopBtn() { const backBtn = document.querySelector('#back-to-top'); if (!backBtn) return; const toTop = () => window.scrollTo({ top: 0 }); const toggle = () => { const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; if (scrollTop > 200 && !backBtn.classList.contains('shown')) { backBtn.classList.add('shown'); backBtn.setAttribute('tabindex', 0); backBtn.addEventListener('click', toTop); } else if (scrollTop <= 200 && backBtn.classList.contains('shown')) { backBtn.classList.remove('shown'); backBtn.setAttribute('tabindex', -1); backBtn.removeEventListener('click', toTop); } }; window.addEventListener('scroll', toggle); toggle(); } function addFootnoteBacklink() { const footnotes = document.querySelectorAll('.footnote-definition'); footnotes.forEach(footnote => { const backlink = document.createElement('button'); backlink.className = 'backlink'; backlink.ariaLabel = 'backlink'; backlink.innerHTML = '↩︎'; backlink.addEventListener('click', () => window.scrollTo({ top: document.querySelector(`.footnote-reference a[href="#${footnote.id}"]`).getBoundingClientRect().top + window.scrollY, })); const lastEl = footnote.lastElementChild || footnote; lastEl.appendChild(backlink); }); } function enableImgLightense() { window.addEventListener("load", () => Lightense(".prose img:not(.no-lightense)", { background: 'rgba(43, 43, 43, 0.19)' })); } function enableReaction() { const container = document.querySelector('.reaction'); if (!container) return; const endpoint = container.dataset.endpoint; const slug = location.pathname.split('/').filter(Boolean).pop(); let state = { error: false, reaction: {} }; const render = () => { const btns = Object.entries(state.reaction).map(([emoji, [count, reacted]])=> { const span = document.createElement('span'); span.textContent = count; const btn = document.createElement('button'); if (reacted) btn.classList.add('reacted'); btn.append(emoji, span); btn.onclick = () => toggle(emoji); return btn; }); if (state.error) { container.classList.add('error'); } else { container.classList.remove('error'); } container.replaceChildren(...btns); }; const toggle = async (target) => { const [count, reacted] = state.reaction[target]; state.reaction[target] = reacted ? [count - 1, false] : [count + 1, true]; render(); try { const resp = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ slug, target, reacted: !reacted }), }); if (resp.status === 200) { error = false; } else { throw new Error(); } } catch (err) { state.error = true; state.reaction[target] = [count, reacted]; render(); } }; const init = async () => { const resp = await fetch(`${endpoint}?slug=${slug}`); if (resp.status === 200) { state.reaction = await resp.json(); render(); } }; init(); } enableThemeToggle(); enablePrerender(); enableRssMask(); if (document.body.classList.contains('post')) { enableOutdateAlert(); addBackToTopBtn(); enableTocTooltip(); } if (document.querySelector('.prose')) { addCopyBtns(); addFootnoteBacklink(); enableImgLightense(); enableReaction(); }