PageSourceSearch

https://profsanjeevarora.github.io/assets/app.js?v=20260327-2

js profsanjeevarora.github.io collected 2026-10-03 09:47:21 UTC 13,875 bytes, 490 lines download raw bytes

1const siteData = window.SITE_DATA;
2
3function createLink(link, className = "") {
4  const anchor = document.createElement("a");
5  anchor.href = link.href;
6  anchor.textContent = link.label;
7  applyLinkBehavior(anchor, link.href);
8
9  if (className) {
10    anchor.className = className;
11  }
12
13  return anchor;
14}
15
16function applyLinkBehavior(element, href) {
17  const isExternal = /^https?:\/\//.test(href);
18  if (isExternal) {
19    element.target = "_blank";
20    element.rel = "noreferrer";
21  }
22}
23
24function createLinkedContainer(className, href) {
25  const anchor = document.createElement("a");
26  anchor.href = href;
27  anchor.className = className;
28  applyLinkBehavior(anchor, href);
29  return anchor;
30}
31
32function createCardHint(text) {
33  const hint = document.createElement("p");
34  hint.className = "card-hint";
35  hint.textContent = text;
36  return hint;
37}
38
39function renderFooter() {
40  const footerNote = document.getElementById("footer-note");
41  if (footerNote) {
42    if (siteData.footerNote) {
43      footerNote.textContent = "";
44      const lines = siteData.footerNote.split("\n");
45      lines.forEach((line, index) => {
46        const span = document.createElement("span");
47        span.className = index === 0 ? "footer-note-line" : "footer-note-credit";
48        span.textContent = line;
49        footerNote.appendChild(span);
50      });
51    } else {
52      footerNote.remove();
53    }
54  }
55}
56
57function renderPageHero(pageKey) {
58  const meta = siteData.pages[pageKey];
59  if (!meta) {
60    return;
61  }
62
63  const title = document.getElementById("page-title");
64  if (title) {
65    title.textContent = meta.title;
66  }
67}
68
69function renderHero() {
70  document.getElementById("hero-name").textContent = siteData.hero.name;
71
72  const heroRoles = document.getElementById("hero-roles");
73  if (heroRoles && siteData.hero.roles) {
74    heroRoles.textContent = "";
75    siteData.hero.roles.forEach((role) => {
76      const line = document.createElement("p");
77      line.className = "hero-role";
78      line.textContent = role;
79      heroRoles.appendChild(line);
80    });
81  }
82
83  const heroInterest = document.getElementById("hero-interest");
84  if (heroInterest && siteData.hero.interest) {
85    heroInterest.textContent = siteData.hero.interest;
86  }
87
88  const heroImage = document.getElementById("hero-image");
89  if (heroImage && siteData.hero.image) {
90    heroImage.src = siteData.hero.image.src;
91    heroImage.alt = siteData.hero.image.alt;
92  }
93
94  const actionsRoot = document.getElementById("hero-actions");
95  siteData.hero.actions.forEach((action) => {
96    actionsRoot.appendChild(createLink(action, `button ${action.variant || "secondary"}`));
97  });
98}
99
100function renderParagraphs(rootId, paragraphs) {
101  const root = document.getElementById(rootId);
102  paragraphs.forEach((paragraph) => {
103    const p = document.createElement("p");
104    p.textContent = paragraph;
105    root.appendChild(p);
106  });
107}
108
109async function fetchTextFile(path) {
110  const response = await fetch(path, { cache: "no-store" });
111  if (!response.ok) {
112    throw new Error(`Failed to load ${path}: ${response.status}`);
113  }
114  return response.text();
115}
116
117function parseMarkdownPeople(markdown) {
118  return markdown
119    .split("\n")
120    .map((line) => line.trim())
121    .filter((line) => line.startsWith("- "))
122    .map((line) => line.slice(2).trim())
123    .map((line) => {
124      const parts = line.split("|").map((part) => part.trim());
125      const primary = parts[0] || "";
126      const note = parts[1] || "";
127      const linkMatch = primary.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
128
129      if (linkMatch) {
130        return { name: linkMatch[1], href: linkMatch[2], note };
131      }
132
133      return { name: primary, note };
134    })
135    .filter((person) => person.name);
136}
137
138function renderCardLinks(rootId, items) {
139  const root = document.getElementById(rootId);
140  items.forEach((item) => {
141    const article = createLinkedContainer("link-card card-link-card", item.href);
142
143    const title = document.createElement("h3");
144    title.textContent = item.title;
145    const description = document.createElement("p");
146    description.textContent = item.description;
147
148    article.append(title, description);
149    root.appendChild(article);
150  });
151}
152
153function renderResearchCards() {
154  const root = document.getElementById("research-cards");
155  siteData.research.forEach((item) => {
156    const primaryLink = item.links && item.links.length === 1 ? item.links[0] : null;
157    const article = primaryLink
158      ? createLinkedContainer("card research-card card-link-card", primaryLink.href)
159      : document.createElement("article");
160
161    if (!primaryLink) {
162      article.className = "card research-card";
163    }
164
165    const title = document.createElement("h3");
166    title.textContent = item.title;
167    const description = document.createElement("p");
168    description.textContent = item.description;
169
170    article.append(title, description);
171
172    if (primaryLink) {
173      article.appendChild(createCardHint(primaryLink.label));
174    } else {
175      const links = document.createElement("div");
176      links.className = "inline-links";
177      item.links.forEach((link) => links.appendChild(createLink(link)));
178      article.appendChild(links);
179    }
180
181    root.appendChild(article);
182  });
183}
184
185function renderStackedList(rootId, items, withLinks = true) {
186  const root = document.getElementById(rootId);
187  items.forEach((item) => {
188    const singleLink = withLinks && item.links && item.links.length === 1 ? item.links[0] : null;
189    const destination = item.href || (singleLink ? singleLink.href : "");
190    const useWholeCardLink = Boolean(destination && (!withLinks || !item.links || item.links.length <= 1));
191    const article = useWholeCardLink
192      ? createLinkedContainer("list-card card-link-card", destination)
193      : document.createElement("article");
194
195    if (!useWholeCardLink) {
196      article.className = "list-card";
197    }
198
199    const title = document.createElement("h3");
200    title.textContent = item.title;
201    const description = document.createElement("p");
202    description.textContent = item.description;
203
204    article.append(title, description);
205
206    if (useWholeCardLink && singleLink) {
207      article.appendChild(createCardHint(singleLink.label));
208    }
209
210    if (withLinks && item.links && item.links.length > 0) {
211      if (item.links.length > 1) {
212        const links = document.createElement("div");
213        links.className = "inline-links";
214        item.links.forEach((link) => links.appendChild(createLink(link)));
215        article.appendChild(links);
216      }
217    }
218
219    if (item.href && !useWholeCardLink) {
220      article.appendChild(createLink({ label: "Visit page", href: item.href }, "inline-link"));
221    }
222
223    root.appendChild(article);
224  });
225}
226
227function renderPersonList(rootId, people) {
228  const root = document.getElementById(rootId);
229  if (!root || !people) {
230    return;
231  }
232
233  people.forEach((person) => {
234    const article = document.createElement("article");
235    article.className = "person-item";
236
237    const name = person.href
238      ? createLink({ label: person.name, href: person.href }, "person-name")
239      : (() => {
240          const span = document.createElement("span");
241          span.className = "person-name";
242          span.textContent = person.name;
243          return span;
244        })();
245
246    article.appendChild(name);
247
248    if (person.note) {
249      const note = document.createElement("p");
250      note.className = "person-note";
251      note.textContent = person.note;
252      article.appendChild(note);
253    }
254
255    root.appendChild(article);
256  });
257}
258
259function renderAdvisingCount(rootId, count) {
260  const root = document.getElementById(rootId);
261  if (!root) {
262    return;
263  }
264
265  root.textContent = `${count} ${count === 1 ? "person" : "people"}`;
266}
267
268function renderAdvisingError(rootId) {
269  const root = document.getElementById(rootId);
270  if (!root) {
271    return;
272  }
273
274  const message = document.createElement("p");
275  message.className = "person-note";
276  message.textContent = "This list could not be loaded.";
277  root.appendChild(message);
278}
279
280function renderProfiles() {
281  const root = document.getElementById("profiles-list");
282  siteData.profiles.forEach((item) => {
283    const article = createLinkedContainer("list-card card-link-card", item.href);
284
285    const title = document.createElement("h3");
286    title.textContent = item.title;
287    const description = document.createElement("p");
288    description.textContent = item.description;
289
290    article.append(title, description);
291    root.appendChild(article);
292  });
293}
294
295function renderContact() {
296  const root = document.getElementById("contact-grid");
297  siteData.contact.forEach((item) => {
298    const article = document.createElement("article");
299    article.className = "contact-card";
300
301    const label = document.createElement("p");
302    label.className = "contact-label";
303    label.textContent = item.label;
304
305    let value;
306    if (item.kind === "link") {
307      value = createLink({ label: item.value, href: item.href }, "contact-value contact-link");
308    } else if (item.kind === "obfuscated-email") {
309      const button = document.createElement("button");
310      button.type = "button";
311      button.className = "contact-value contact-reveal";
312      button.textContent = item.revealLabel || "Click to reveal";
313      button.addEventListener("click", () => {
314        const address = `${item.user}@${item.domain.join(".")}`;
315        const anchor = document.createElement("a");
316        anchor.className = "contact-value contact-link";
317        anchor.href = `mailto:${address}`;
318        anchor.textContent = address;
319        button.replaceWith(anchor);
320      });
321      value = button;
322    } else {
323      value = document.createElement("p");
324      value.className = "contact-value";
325      value.textContent = item.value;
326    }
327
328    article.append(label, value);
329    root.appendChild(article);
330  });
331}
332
333function setupNavToggle() {
334  const button = document.querySelector(".nav-toggle");
335  const nav = document.getElementById("site-nav");
336  if (!button || !nav) {
337    return;
338  }
339
340  const closeNav = () => {
341    button.setAttribute("aria-expanded", "false");
342    nav.classList.remove("open");
343  };
344
345  button.addEventListener("click", () => {
346    const expanded = button.getAttribute("aria-expanded") === "true";
347    if (expanded) {
348      closeNav();
349      return;
350    }
351
352    button.setAttribute("aria-expanded", "true");
353    nav.classList.add("open");
354  });
355
356  nav.querySelectorAll("a").forEach((link) => {
357    link.addEventListener("click", closeNav);
358  });
359
360  document.addEventListener("click", (event) => {
361    if (nav.classList.contains("open") && !nav.contains(event.target) && !button.contains(event.target)) {
362      closeNav();
363    }
364  });
365
366  document.addEventListener("keydown", (event) => {
367    if (event.key === "Escape") {
368      closeNav();
369    }
370  });
371
372  window.addEventListener("resize", () => {
373    if (window.innerWidth > 860) {
374      closeNav();
375    }
376  });
377}
378
379function markCurrentNav() {
380  const currentPage = window.location.pathname.split("/").pop() || "index.html";
381  document.querySelectorAll("#site-nav a").forEach((link) => {
382    const linkPage = new URL(link.href, window.location.href).pathname.split("/").pop();
383    if (linkPage === currentPage) {
384      link.setAttribute("aria-current", "page");
385    }
386  });
387}
388
389function renderHome() {
390  renderHero();
391  renderCardLinks("home-links", siteData.homeLinks);
392}
393
394function renderResearchPage() {
395  renderPageHero("research");
396  renderResearchCards();
397}
398
399function renderTeachingPage() {
400  renderPageHero("teaching");
401  renderStackedList("teaching-list", siteData.teaching);
402}
403
404function renderArchiveCoursesPage() {
405  renderPageHero("archived-courses");
406  renderStackedList("archive-courses-list", siteData.archiveCourses, false);
407}
408
409function renderBioPage() {
410  renderPageHero("bio");
411
412  const root = document.getElementById("bio-sections");
413  siteData.bioSections.forEach((section) => {
414    const wrapper = document.createElement("section");
415    wrapper.className = "content-section bio-section-card";
416
417    const heading = document.createElement("h2");
418    heading.className = "bio-section-title";
419    heading.textContent = section.title;
420
421    const list = document.createElement("ul");
422    list.className = "bio-list";
423
424    section.items.forEach((item) => {
425      const li = document.createElement("li");
426      if (item.html) {
427        li.innerHTML = item.html;
428      } else {
429        li.textContent = item.text;
430      }
431      list.appendChild(li);
432    });
433
434    wrapper.append(heading, list);
435    root.appendChild(wrapper);
436  });
437}
438
439async function renderAdvisingPage() {
440  renderPageHero("advising");
441
442  try {
443    const [currentMarkdown, alumniMarkdown] = await Promise.all([
444      fetchTextFile(siteData.advisingFiles.currentStudents),
445      fetchTextFile(siteData.advisingFiles.alumni),
446    ]);
447
448    const currentPeople = parseMarkdownPeople(currentMarkdown);
449    const alumniPeople = parseMarkdownPeople(alumniMarkdown);
450
451    renderPersonList("current-students-list", currentPeople);
452    renderPersonList("alumni-list", alumniPeople);
453    renderAdvisingCount("current-students-count", currentPeople.length);
454    renderAdvisingCount("alumni-count", alumniPeople.length);
455  } catch (error) {
456    console.error(error);
457    renderAdvisingError("current-students-list");
458    renderAdvisingError("alumni-list");
459  }
460}
461
462function renderContactPage() {
463  renderPageHero("contact");
464  renderContact();
465}
466
467function init() {
468  const page = document.body.dataset.page;
469
470  setupNavToggle();
471  markCurrentNav();
472  renderFooter();
473
474  const pageRenderers = {
475    home: renderHome,
476    bio: renderBioPage,
477    research: renderResearchPage,
478    teaching: renderTeachingPage,
479    "archived-courses": renderArchiveCoursesPage,
480    advising: renderAdvisingPage,
481    contact: renderContactPage,
482  };
483
484  const renderPage = pageRenderers[page];
485  if (renderPage) {
486    Promise.resolve(renderPage()).catch((error) => console.error(error));
487  }
488}
489
490init();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.