FineVision / app /src /pages /index.astro
thibaud frere
update template
d15ea6f
raw
history blame
9.48 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';
import TableOfContents from '../components/TableOfContents.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 ?? '';
// Accept authors as string[] or array of objects { name, url, affiliations? }
const rawAuthors = (articleFM as any)?.authors ?? [];
type Affiliation = { id: number; name: string; url?: string };
type Author = { name: string; url?: string; affiliationIndices?: number[] };
// Normalize affiliations from frontmatter: supports strings or objects { id?, name, url? }
const rawAffils = (articleFM as any)?.affiliations ?? (articleFM as any)?.affiliation ?? [];
const normalizedAffiliations: Affiliation[] = (() => {
const seen: Map<string, number> = new Map();
const list: Affiliation[] = [];
const pushUnique = (name: string, url?: string) => {
const key = `${String(name).trim()}|${url ? String(url).trim() : ''}`;
if (seen.has(key)) return seen.get(key)!;
const id = list.length + 1;
list.push({ id, name: String(name).trim(), url: url ? String(url) : undefined });
seen.set(key, id);
return id;
};
const input = Array.isArray(rawAffils) ? rawAffils : (rawAffils ? [rawAffils] : []);
for (const a of input) {
if (typeof a === 'string') {
pushUnique(a);
} else if (a && typeof a === 'object') {
const name = a.name ?? a.label ?? a.text ?? a.affiliation ?? '';
if (!String(name).trim()) continue;
const url = a.url || a.link;
// Respect provided numeric id for display stability if present and sequential; otherwise reassign
pushUnique(String(name), url ? String(url) : undefined);
}
}
return list;
})();
// Helper: ensure an affiliation exists and return its id
const ensureAffiliation = (val: any): number | undefined => {
if (val == null) return undefined;
if (typeof val === 'number' && Number.isFinite(val) && val > 0) {
return Math.floor(val);
}
const name = typeof val === 'string' ? val : (val?.name ?? val?.label ?? val?.text ?? val?.affiliation);
if (!name || !String(name).trim()) return undefined;
const existing = normalizedAffiliations.find(a => a.name === String(name).trim());
if (existing) return existing.id;
const id = normalizedAffiliations.length + 1;
normalizedAffiliations.push({ id, name: String(name).trim(), url: val?.url || val?.link });
return id;
};
// Normalize authors and map affiliations -> indices (Distill-like)
const normalizedAuthors: Author[] = (Array.isArray(rawAuthors) ? rawAuthors : [])
.map((a: any) => {
if (typeof a === 'string') {
return { name: a } as Author;
}
const name = String(a?.name || '').trim();
const url = a?.url || a?.link;
let indices: number[] | undefined = undefined;
const raw = a?.affiliations ?? a?.affiliation ?? a?.affils;
if (raw != null) {
const entries = Array.isArray(raw) ? raw : [raw];
const ids = entries.map(ensureAffiliation).filter((x): x is number => typeof x === 'number');
const unique = Array.from(new Set(ids)).sort((x, y) => x - y);
if (unique.length) indices = unique;
}
return { name, url, affiliationIndices: indices } as Author;
})
.filter((a: Author) => a.name && a.name.trim().length > 0);
const authorNames: string[] = normalizedAuthors.map(a => a.name);
const published = articleFM?.published ?? undefined;
const tags = articleFM?.tags ?? [];
// Prefer seoThumbImage from frontmatter if provided
const fmOg = articleFM?.seoThumbImage 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 = authorNames.join(', ');
const citationText = `${citationAuthorsText}${year ? ` (${year})` : ''}. "${titleFlat}".`;
const authorsBib = authorNames.join(' and ');
const keyAuthor = (authorNames[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 doi = (ArticleMod as any)?.frontmatter?.doi ? String((ArticleMod as any).frontmatter.doi) : undefined;
const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBib}},\n ${year ? `year={${year}},\n ` : ''}${doi ? `doi={${doi}}` : ''}\n}`;
const envCollapse = false;
const tableOfContentAutoCollapse = Boolean(
(articleFM as any)?.tableOfContentAutoCollapse ?? (articleFM as any)?.tableOfContentsAutoCollapse ?? envCollapse
);
// Licence note (HTML allowed)
const licence = (articleFM as any)?.licence ?? (articleFM as any)?.license ?? (articleFM as any)?.licenseNote;
---
<html lang="en" data-theme="light" data-toc-auto-collapse={tableOfContentAutoCollapse ? '1' : '0'}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Seo title={docTitle} description={description} authors={authorNames} 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 is:inline src="/scripts/color-palettes.js"></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 src="https://cdn.jsdelivr.net/npm/[email protected]/dist/medium-zoom.min.js"></script>
</head>
<body>
<ThemeToggle />
<Hero title={docTitleHtml} titleRaw={docTitle} description={subtitle} authors={normalizedAuthors as any} affiliations={normalizedAffiliations as any} affiliation={articleFM?.affiliation} published={articleFM?.published} doi={doi} />
<section class="content-grid">
<TableOfContents tableOfContentAutoCollapse={tableOfContentAutoCollapse} />
<main>
<Article />
</main>
</section>
<Footer citationText={citationText} bibtex={bibtex} licence={licence} doi={doi} />
<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>
</body>
</html>