| --- |
| 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"; |
| |
| 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"; |
| |
| const docTitleHtml = (articleFM?.title ?? "Untitled article") |
| .replace(/\\n/g, "<br/>") |
| .replace(/\n/g, "<br/>"); |
| const subtitle = articleFM?.subtitle ?? ""; |
| const description = articleFM?.description ?? ""; |
| |
| const rawAuthors = (articleFM as any)?.authors ?? []; |
| type Affiliation = { id: number; name: string; url?: string }; |
| type Author = { name: string; url?: string; affiliationIndices?: number[] }; |
|
|
| |
| 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; |
| |
| pushUnique(String(name), url ? String(url) : undefined); |
| } |
| } |
| return list; |
| })(); |
|
|
| |
| 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; |
| }; |
|
|
| |
| 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 ?? []; |
| |
| 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); |
|
|
| |
| 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, |
| ); |
| |
| 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> |
| |
| |
| <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/medium-zoom@1.1.0/dist/medium-zoom.min.js" |
| ></script> |
| <script> |
| |
| |
| function initializeZoom() { |
| const zoomableImages = document.querySelectorAll( |
| 'img[data-zoomable="1"]', |
| ); |
| |
| if (window.mediumZoom && zoomableImages.length > 0) { |
| zoomableImages.forEach((img, index) => { |
| |
| 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", |
| ); |
| } |
| } |
| |
| |
| if (document.readyState === "loading") { |
| document.addEventListener("DOMContentLoaded", initializeZoom); |
| } else { |
| initializeZoom(); |
| } |
| |
| |
| 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> |
| |
| 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> |
| |
| 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> |
|
|