---
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;
---