--- 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; // Helper to strip HTML tags const stripHtml = (text: string) => String(text || "").replace(/<[^>]*>/g, ""); // docTitle without HTML tags for SEO/meta const docTitle = stripHtml(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 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, ""); 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; ---