---
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.auto.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, '
')
.replace(/\n/g, '
');
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 = 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 stripHtml = (text: string) => String(text || '').replace(/<[^>]*>/g, '');
const rawTitle = articleFM?.title ?? 'Untitled article';
const titleFlat = stripHtml(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;
---