Spaces:
Running
Running
| --- | |
| 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'; | |
| // Default OG image served from public/ | |
| 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'; | |
| // Allow explicit line breaks in the title via "\n" or YAML newlines | |
| 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 ?? []; | |
| // Prefer ogImage from frontmatter if provided | |
| 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)); | |
| // ---- Build citation text & BibTeX from frontmatter ---- | |
| 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> | |
| <!-- TO MANAGE PROPERLY --> | |
| <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> | |
| /* Inline tweak for details blocks used in MDX */ | |
| details { background: var(--code-bg) ; border: 1px solid var(--border-color) ; border-radius: 6px; margin: 1em 0; padding: .5em .75em; } | |
| </style> | |
| </main> | |
| </section> | |
| <Footer citationText={citationText} bibtex={bibtex} /> | |
| <!-- Medium-like image zoom (lightbox) --> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/medium-zoom.min.js"></script> | |
| <script> | |
| // Initialize zoom on img[data-zoomable]; wait for script & content; close on scroll like Medium | |
| (() => { | |
| /** @type {any} */ | |
| let zoomInstance = null; | |
| /** @param {() => void} cb */ | |
| const ensureMediumZoomReady = (cb) => { | |
| // @ts-ignore mediumZoom injected globally by external script | |
| if (window.mediumZoom) return cb(); | |
| // @ts-ignore mediumZoom injected globally by external script | |
| const retry = () => (window.mediumZoom ? cb() : setTimeout(retry, 30)); | |
| retry(); | |
| }; | |
| /** @returns {HTMLElement[]} */ | |
| 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) { | |
| // @ts-ignore medium-zoom injected globally by external script | |
| zoomInstance = window.mediumZoom(targets, { background, margin: 24, scrollOffset: 0 }); | |
| let onScrollLike; | |
| const attachCloseOnScroll = () => { | |
| if (onScrollLike) return; | |
| // @ts-ignore medium-zoom instance has close() | |
| 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; | |
| }; | |
| // @ts-ignore medium-zoom instance has on() | |
| zoomInstance.on('open', attachCloseOnScroll); | |
| // @ts-ignore medium-zoom instance has on() | |
| zoomInstance.on('close', detachCloseOnScroll); | |
| const themeObserver = new MutationObserver(() => { | |
| const dark = document.documentElement.getAttribute('data-theme') === 'dark'; | |
| // @ts-ignore medium-zoom instance has update() | |
| 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 { | |
| // @ts-ignore medium-zoom instance has attach()/update() | |
| zoomInstance.attach(targets); | |
| // @ts-ignore medium-zoom instance has update() | |
| 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> | |
| // Add a small download button to img[data-downloadable] | |
| (() => { | |
| const SELECTOR = 'section.content-grid main img[data-downloadable]'; | |
| /** | |
| * @param {HTMLImageElement} img | |
| */ | |
| 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'; | |
| // Wrap the image in a positioned inline-block so the button is on the image | |
| 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'); | |
| } | |
| // Determine download href and filename | |
| const pickHrefAndName = () => { | |
| const current = img.currentSrc || img.src || ''; | |
| let href = img.getAttribute('data-download-src') || current; | |
| // Derive filename from the original source when possible | |
| const deriveName = () => { | |
| try { | |
| const u = new URL(current, location.href); | |
| // Prefer original href param if provided by Astro image service | |
| const rawHref = u.searchParams.get('href'); | |
| const candidate = rawHref ? decodeURIComponent(rawHref) : u.pathname; | |
| const last = String(candidate).split('/').pop() || ''; | |
| // Strip query/hash and any appended transform suffixes after extension | |
| 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]; | |
| // If extension missing, fallback to base as-is | |
| 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>'; | |
| // Ensure href/name match currentSrc right before navigation | |
| 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 { | |
| // Fallback to native behavior if fetch fails | |
| } | |
| }); | |
| // Append inside wrapper so positioning is relative to the image box | |
| 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> | |
| // Open external links in a new tab; keep internal anchors in-page | |
| 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> | |
| // Delegate copy clicks for code blocks injected by rehypeCodeCopyAndLabel | |
| 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> | |
| // Build TOC from article headings (h2/h3/h4) and render into the sticky aside | |
| const buildTOC = () => { | |
| const holder = document.getElementById('article-toc-placeholder'); | |
| const holderMobile = document.getElementById('article-toc-mobile-placeholder'); | |
| // Always rebuild TOC to avoid stale entries | |
| 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; | |
| // Filter out headings that should not appear in TOC | |
| 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; | |
| // Ensure unique ids for headings (deduplicate duplicates) | |
| 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); | |
| // adjust depth | |
| 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)); | |
| // active link on scroll | |
| 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(); | |
| // Close mobile accordion when a link inside it is clicked | |
| 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> | |
| <!-- Removed JS fallback for language chips; labels handled by CSS/Shiki --> | |
| </body> | |
| </html> | |