| --- |
| import * as ArticleMod from '../content/article.mdx'; |
| import Hero from '../components/Hero.astro'; |
| import Footer from '../components/Footer.astro'; |
| import ThemeToggle from '../components/ThemeToggle.astro'; |
| import Seo from '../components/Seo.astro'; |
| |
| const ogDefaultUrl = '/thumb.jpg'; |
| import 'katex/dist/katex.min.css'; |
| import '../styles/global.css'; |
| const articleFM = (ArticleMod as any).frontmatter ?? {}; |
| const Article = (ArticleMod as any).default; |
| const docTitle = articleFM?.title ?? 'Untitled article'; |
| |
| const docTitleHtml = (articleFM?.title ?? 'Untitled article') |
| .replace(/\\n/g, '<br/>') |
| .replace(/\n/g, '<br/>'); |
| const subtitle = articleFM?.subtitle ?? ''; |
| const description = articleFM?.description ?? ''; |
| const authors = articleFM?.authors ?? []; |
| const published = articleFM?.published ?? undefined; |
| const tags = articleFM?.tags ?? []; |
| |
| const fmOg = articleFM?.ogImage as string | undefined; |
| const imageAbs: string = fmOg && fmOg.startsWith('http') |
| ? fmOg |
| : (Astro.site ? new URL((fmOg ?? ogDefaultUrl), Astro.site).toString() : (fmOg ?? ogDefaultUrl)); |
|
|
| |
| const rawTitle = articleFM?.title ?? 'Untitled article'; |
| const titleFlat = String(rawTitle) |
| .replace(/\\n/g, ' ') |
| .replace(/\n/g, ' ') |
| .replace(/\s+/g, ' ') |
| .trim(); |
| const extractYear = (val: string | undefined): number | undefined => { |
| if (!val) return undefined; |
| const d = new Date(val); |
| if (!Number.isNaN(d.getTime())) return d.getFullYear(); |
| const m = String(val).match(/(19|20)\d{2}/); |
| return m ? Number(m[0]) : undefined; |
| }; |
|
|
| const year = extractYear(published); |
| const citationAuthorsText = authors.join(', '); |
| const citationText = `${citationAuthorsText}${year ? ` (${year})` : ''}. "${titleFlat}".`; |
|
|
| const authorsBib = authors.join(' and '); |
| const keyAuthor = (authors[0] || 'article').split(/\s+/).slice(-1)[0].toLowerCase(); |
| const keyTitle = titleFlat.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '').slice(0, 24); |
| const bibKey = `${keyAuthor}${year ?? ''}_${keyTitle}`; |
| const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBib}},\n ${year ? `year={${year}}` : ''}\n}`; |
| --- |
| <html lang="en" data-theme="light"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <Seo title={docTitle} description={description} authors={authors} published={published} tags={tags} image={imageAbs} /> |
| <script is:inline> |
| (() => { |
| try { |
| const saved = localStorage.getItem('theme'); |
| const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; |
| const theme = saved || (prefersDark ? 'dark' : 'light'); |
| document.documentElement.setAttribute('data-theme', theme); |
| } catch {} |
| })(); |
| </script> |
| |
| |
| <script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"></script> |
| <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script> |
| <script type="module" src="https://gradio.s3-us-west-2.amazonaws.com/4.4.0/gradio.js"> </script> |
| </head> |
| <body> |
| <ThemeToggle /> |
| <Hero title={docTitleHtml} titleRaw={docTitle} description={subtitle} authors={articleFM?.authors} affiliation={articleFM?.affiliation} published={articleFM?.published} /> |
| |
| <section class="content-grid"> |
| <aside class="toc"> |
| <div class="title">Table of Contents</div> |
| <div id="article-toc-placeholder"></div> |
| </aside> |
| <details class="toc-mobile"> |
| <summary>Table of Contents</summary> |
| <div id="article-toc-mobile-placeholder"></div> |
| </details> |
| <main> |
| <Article /> |
| <style is:inline> |
| |
| details { background: var(--code-bg) !important; border: 1px solid var(--border-color) !important; border-radius: 6px; margin: 1em 0; padding: .5em .75em; } |
| </style> |
| </main> |
| </section> |
| |
| <Footer citationText={citationText} bibtex={bibtex} /> |
| |
| |
| <script src="https://cdn.jsdelivr.net/npm/medium-zoom@1.1.0/dist/medium-zoom.min.js"></script> |
| <script> |
| |
| (() => { |
| |
| let zoomInstance = null; |
| |
| |
| const ensureMediumZoomReady = (cb) => { |
| |
| if (window.mediumZoom) return cb(); |
| |
| const retry = () => (window.mediumZoom ? cb() : setTimeout(retry, 30)); |
| retry(); |
| }; |
| |
| |
| const collectTargets = () => Array.from(document.querySelectorAll('section.content-grid main img[data-zoomable]')); |
| |
| const initOrUpdateZoom = () => { |
| const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; |
| const background = isDark ? 'rgba(0,0,0,.9)' : 'rgba(0,0,0,.85)'; |
| const targets = collectTargets(); |
| if (!targets.length) return; |
| |
| if (!zoomInstance) { |
| |
| zoomInstance = window.mediumZoom(targets, { background, margin: 24, scrollOffset: 0 }); |
| |
| let onScrollLike; |
| const attachCloseOnScroll = () => { |
| if (onScrollLike) return; |
| |
| onScrollLike = () => { zoomInstance && zoomInstance.close(); }; |
| window.addEventListener('wheel', onScrollLike, { passive: true }); |
| window.addEventListener('touchmove', onScrollLike, { passive: true }); |
| window.addEventListener('scroll', onScrollLike, { passive: true }); |
| }; |
| const detachCloseOnScroll = () => { |
| if (!onScrollLike) return; |
| window.removeEventListener('wheel', onScrollLike); |
| window.removeEventListener('touchmove', onScrollLike); |
| window.removeEventListener('scroll', onScrollLike); |
| onScrollLike = null; |
| }; |
| |
| zoomInstance.on('open', attachCloseOnScroll); |
| |
| zoomInstance.on('close', detachCloseOnScroll); |
| |
| const themeObserver = new MutationObserver(() => { |
| const dark = document.documentElement.getAttribute('data-theme') === 'dark'; |
| |
| zoomInstance && zoomInstance.update({ background: dark ? 'rgba(0,0,0,.9)' : 'rgba(0,0,0,.85)' }); |
| }); |
| themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); |
| } else { |
| |
| zoomInstance.attach(targets); |
| |
| zoomInstance.update({ background }); |
| } |
| }; |
| |
| const bootstrap = () => ensureMediumZoomReady(() => { |
| initOrUpdateZoom(); |
| setTimeout(initOrUpdateZoom, 0); |
| const main = document.querySelector('section.content-grid main'); |
| if (main) { |
| const mo = new MutationObserver(() => initOrUpdateZoom()); |
| mo.observe(main, { childList: true, subtree: true }); |
| } |
| }); |
| |
| if (document.readyState === 'complete') bootstrap(); |
| else window.addEventListener('load', bootstrap, { once: true }); |
| })(); |
| </script> |
| |
| |
| <script> |
| |
| (() => { |
| const SELECTOR = 'section.content-grid main img[data-downloadable]'; |
| |
| |
| |
| |
| const injectDownloadButton = (img) => { |
| if (!img || img.dataset.__dlInjected) return; |
| const parentFigure = img.closest('figure'); |
| const parent = img.parentElement; |
| if (!parent) return; |
| img.dataset.__dlInjected = '1'; |
| |
| |
| const wrapper = document.createElement('span'); |
| wrapper.className = 'img-dl-wrap'; |
| parent.insertBefore(wrapper, img); |
| wrapper.appendChild(img); |
| if (parentFigure && !parentFigure.classList.contains('has-dl-btn')) { |
| parentFigure.classList.add('has-dl-btn'); |
| } |
| |
| |
| const pickHrefAndName = () => { |
| const current = img.currentSrc || img.src || ''; |
| let href = img.getAttribute('data-download-src') || current; |
| |
| const deriveName = () => { |
| try { |
| const u = new URL(current, location.href); |
| |
| const rawHref = u.searchParams.get('href'); |
| const candidate = rawHref ? decodeURIComponent(rawHref) : u.pathname; |
| const last = String(candidate).split('/').pop() || ''; |
| |
| const base = last.split('?')[0].split('#')[0]; |
| const m = base.match(/^(.+?\.(?:png|jpe?g|webp|avif|gif|svg))(?:[._-].*)?$/i); |
| if (m && m[1]) return m[1]; |
| |
| return base || 'image'; |
| } catch { |
| return 'image'; |
| } |
| }; |
| const name = img.getAttribute('data-download-name') || deriveName(); |
| return { href, name }; |
| }; |
| |
| const { href, name } = pickHrefAndName(); |
| const a = document.createElement('a'); |
| a.className = 'button button--ghost img-dl-btn'; |
| a.href = href; |
| if (name) a.download = name; |
| a.setAttribute('aria-label', 'Download image'); |
| a.setAttribute('title', name ? `Download ${name}` : 'Download image'); |
| a.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 16c-.26 0-.52-.11-.71-.29l-5-5a1 1 0 0 1 1.42-1.42L11 12.59V4a1 1 0 1 1 2 0v8.59l3.29-3.3a1 1 0 1 1 1.42 1.42l-5 5c-.19.18-.45.29-.71.29zM5 20a1 1 0 1 1 0-2h14a1 1 0 1 1 0 2H5z"/></svg>'; |
| |
| |
| a.addEventListener('click', async (ev) => { |
| try { |
| ev.preventDefault(); |
| const picked = pickHrefAndName(); |
| const res = await fetch(picked.href, { credentials: 'same-origin' }); |
| const blob = await res.blob(); |
| const objectUrl = URL.createObjectURL(blob); |
| const tmp = document.createElement('a'); |
| tmp.href = objectUrl; |
| tmp.download = picked.name || 'image'; |
| document.body.appendChild(tmp); |
| tmp.click(); |
| setTimeout(() => { URL.revokeObjectURL(objectUrl); tmp.remove(); }, 1000); |
| } catch { |
| |
| } |
| }); |
| |
| |
| wrapper.appendChild(a); |
| }; |
| |
| const scan = () => { |
| document.querySelectorAll(SELECTOR).forEach((el) => injectDownloadButton(el)); |
| }; |
| |
| const bootstrap = () => { |
| scan(); |
| const main = document.querySelector('section.content-grid main'); |
| if (!main) return; |
| const mo = new MutationObserver(() => scan()); |
| mo.observe(main, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); |
| }; |
| |
| if (document.readyState === 'complete') bootstrap(); |
| else window.addEventListener('load', bootstrap, { once: true }); |
| })(); |
| </script> |
| |
| <script> |
| |
| const setExternalTargets = () => { |
| const isExternal = (href) => { |
| try { const u = new URL(href, location.href); return u.origin !== location.origin; } catch { return false; } |
| }; |
| document.querySelectorAll('a[href]').forEach(a => { |
| const href = a.getAttribute('href'); |
| if (!href) return; |
| if (isExternal(href)) { |
| a.setAttribute('target', '_blank'); |
| a.setAttribute('rel', 'noopener noreferrer'); |
| } else { |
| a.removeAttribute('target'); |
| } |
| }); |
| }; |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', setExternalTargets, { once: true }); |
| } else { setExternalTargets(); } |
| </script> |
| |
| <script> |
| |
| document.addEventListener('click', async (e) => { |
| const target = e.target instanceof Element ? e.target : null; |
| const btn = target ? target.closest('.code-copy') : null; |
| if (!btn) return; |
| const card = btn.closest('.code-card'); |
| const pre = card && card.querySelector('pre'); |
| if (!pre) return; |
| const text = pre.textContent || ''; |
| try { |
| await navigator.clipboard.writeText(text.trim()); |
| const old = btn.innerHTML; |
| btn.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 16.2l-3.5-3.5-1.4 1.4L9 19 20.3 7.7l-1.4-1.4z"/></svg>'; |
| setTimeout(() => (btn.innerHTML = old), 1200); |
| } catch { |
| btn.textContent = 'Error'; |
| setTimeout(() => (btn.textContent = 'Copy'), 1200); |
| } |
| }); |
| </script> |
| |
| <script> |
| |
| const buildTOC = () => { |
| const holder = document.getElementById('article-toc-placeholder'); |
| const holderMobile = document.getElementById('article-toc-mobile-placeholder'); |
| |
| if (holder) holder.innerHTML = ''; |
| if (holderMobile) holderMobile.innerHTML = ''; |
| const articleRoot = document.querySelector('section.content-grid main'); |
| if (!articleRoot) return; |
| const headings = articleRoot.querySelectorAll('h2, h3, h4'); |
| if (!headings.length) return; |
| |
| |
| const normalize = (s) => String(s || '') |
| .toLowerCase() |
| .replace(/[^a-z0-9]+/g, ' ') |
| .trim(); |
| const isTocLabel = (s) => /^(table\s+of\s+contents?)$|^toc$/i.test(String(s || '').replace(/[^a-zA-Z0-9]+/g, ' ').trim()); |
| const shouldSkip = (h) => { |
| const t = h.textContent || ''; |
| const id = String(h.id || ''); |
| const slug = normalize(t).replace(/\s+/g, '_'); |
| if (isTocLabel(t)) return true; |
| if (isTocLabel(id.replace(/[_-]+/g, ' '))) return true; |
| if (isTocLabel(slug.replace(/[_-]+/g, ' '))) return true; |
| return false; |
| }; |
| const headingsArr = Array.from(headings).filter(h => !shouldSkip(h)); |
| if (!headingsArr.length) return; |
| |
| |
| const usedIds = new Set<string>(); |
| const slugify = (s: string) => String(s || '') |
| .toLowerCase() |
| .trim() |
| .replace(/\s+/g, '_') |
| .replace(/[^a-z0-9_\-]/g, ''); |
| headingsArr.forEach((h) => { |
| let id = (h.id || '').trim(); |
| if (!id) { |
| const base = slugify(h.textContent || ''); |
| id = base || 'section'; |
| } |
| let candidate = id; |
| let n = 2; |
| while (usedIds.has(candidate)) { |
| candidate = `${id}-${n++}`; |
| } |
| if (h.id !== candidate) h.id = candidate; |
| usedIds.add(candidate); |
| }); |
| |
| const nav = document.createElement('nav'); |
| let ulStack = [document.createElement('ul')]; |
| nav.appendChild(ulStack[0]); |
| |
| const levelOf = (tag) => tag === 'H2' ? 2 : tag === 'H3' ? 3 : 4; |
| let prev = 2; |
| headingsArr.forEach((h) => { |
| const lvl = levelOf(h.tagName); |
| |
| while (lvl > prev) { const ul = document.createElement('ul'); ulStack[ulStack.length-1].lastElementChild?.appendChild(ul); ulStack.push(ul); prev++; } |
| while (lvl < prev) { ulStack.pop(); prev--; } |
| const li = document.createElement('li'); |
| const a = document.createElement('a'); |
| a.href = '#' + h.id; a.textContent = h.textContent; a.target = '_self'; |
| li.appendChild(a); |
| ulStack[ulStack.length-1].appendChild(li); |
| }); |
| |
| if (holder) holder.appendChild(nav); |
| if (holderMobile) holderMobile.appendChild(nav.cloneNode(true)); |
| |
| |
| const links = [ |
| ...(holder ? holder.querySelectorAll('a') : []), |
| ...(holderMobile ? holderMobile.querySelectorAll('a') : []) |
| ]; |
| const onScroll = () => { |
| for (let i = headingsArr.length - 1; i >= 0; i--) { |
| const top = headingsArr[i].getBoundingClientRect().top; |
| if (top - 60 <= 0) { |
| links.forEach(l => l.classList.remove('active')); |
| const id = '#' + headingsArr[i].id; |
| const actives = Array.from(links).filter(l => l.getAttribute('href') === id); |
| actives.forEach(a => a.classList.add('active')); |
| break; |
| } |
| } |
| }; |
| window.addEventListener('scroll', onScroll); |
| onScroll(); |
| |
| |
| if (holderMobile) { |
| const details = holderMobile.closest('details'); |
| holderMobile.addEventListener('click', (ev) => { |
| const target = ev.target as Element | null; |
| const anchor = target && 'closest' in target ? (target as Element).closest('a') : null; |
| if (anchor instanceof HTMLAnchorElement && details && (details as HTMLDetailsElement).open) { |
| (details as HTMLDetailsElement).open = false; |
| } |
| }); |
| } |
| }; |
| |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', buildTOC, { once: true }); |
| } else { buildTOC(); } |
| </script> |
| |
| |
| </body> |
| </html> |
| |
| |
| |