File size: 9,476 Bytes
400357a
72cfb5a
 
fee9c1e
 
72cfb5a
a4b9560
52307d3
 
101af31
b9e7b9b
72cfb5a
 
fee9c1e
e329457
fee9c1e
 
 
b2ac38d
fee9c1e
a4b9560
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fee9c1e
 
a4b9560
 
72cfb5a
fee9c1e
72cfb5a
fc7711a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a4b9560
fc7711a
 
a4b9560
 
fc7711a
 
a4b9560
 
 
c1d1666
a4b9560
c1d1666
a4b9560
 
400357a
c1d1666
400357a
 
 
a4b9560
fef84f3
 
 
 
 
 
 
 
 
 
c1d1666
fef84f3
9e27095
3f64d97
e0ad823
c1d1666
 
400357a
 
fee9c1e
a4b9560
fee9c1e
 
a4b9560
fee9c1e
 
 
 
 
a4b9560
fee9c1e
b8e1b6c
fee9c1e
 
 
 
 
 
 
 
 
 
 
 
 
 
3f64d97
fee9c1e
 
 
 
 
 
 
72cfb5a
0390c56
72cfb5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c1d1666
400357a
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
---
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.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 rawTitle = articleFM?.title ?? 'Untitled article';
const titleFlat = 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 is:inline 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/medium-zoom@1.1.0/dist/medium-zoom.min.js"></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} />

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