FineVision / app /src /pages /index.astro
thibaud frere
update
52307d3
raw
history blame
19.1 kB
---
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) !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} />
<!-- 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>