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.