Spaces:
Running
Running
| --- | |
| 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, "<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 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; | |
| --- | |
| <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 type="module" 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> | |
| <script> | |
| // Debug and global zoom initialization | |
| function initializeZoom() { | |
| const zoomableImages = document.querySelectorAll( | |
| 'img[data-zoomable="1"]', | |
| ); | |
| if (window.mediumZoom && zoomableImages.length > 0) { | |
| zoomableImages.forEach((img, index) => { | |
| // Check if already initialized | |
| if (!img.classList.contains("medium-zoom-image")) { | |
| try { | |
| const instance = window.mediumZoom(img, { | |
| background: "rgba(0,0,0,.85)", | |
| margin: 24, | |
| scrollOffset: 0, | |
| }); | |
| } catch (error) { | |
| console.error( | |
| `Global script: Error initializing zoom for image ${index}:`, | |
| error, | |
| ); | |
| } | |
| } else { | |
| console.log(`Global script: Image ${index} already has zoom`); | |
| } | |
| }); | |
| } else { | |
| console.log( | |
| "Global script: mediumZoom not available or no images found", | |
| ); | |
| } | |
| } | |
| // Try to initialize immediately | |
| if (document.readyState === "loading") { | |
| document.addEventListener("DOMContentLoaded", initializeZoom); | |
| } else { | |
| initializeZoom(); | |
| } | |
| // Also try after complete loading | |
| window.addEventListener("load", () => { | |
| setTimeout(initializeZoom, 100); | |
| }); | |
| </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} | |
| pdfProOnly={articleFM?.pdfProOnly} | |
| /> | |
| <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 rehypeCodeCopy | |
| 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> | |