1(function () { 2 "use strict"; 3 4 const data = window.RESTUD_TOUR_DATA; 5 if (!data || !data.upcomingTour) { 6 return; 7 } 8 9 const socialIcons = { 10 "x-twitter": "fa-brands fa-x-twitter", 11 twitter: "fa-brands fa-x-twitter", 12 linkedin: "fa-brands fa-linkedin-in", 13 github: "fa-brands fa-github", 14 website: "fa fa-globe", 15 homepage: "fa fa-globe" 16 }; 17 18 function byId(id) { 19 return document.getElementById(id); 20 } 21 22 function escapeHtml(value) { 23 return String(value || "") 24 .replace(/&/g, "&") 25 .replace(/</g, "<") 26 .replace(/>/g, ">") 27 .replace(/"/g, """) 28 .replace(/'/g, "'"); 29 } 30 31 function escapeAttr(value) { 32 return escapeHtml(value).replace(/`/g, "`"); 33 } 34 35 function textToHtml(value) { 36 return escapeHtml(value).replace(/\n/g, "<br>"); 37 } 38 39 function safeId(value) { 40 return String(value || "") 41 .toLowerCase() 42 .replace(/[^a-z0-9]+/g, "-") 43 .replace(/^-+|-+$/g, ""); 44 } 45 46 function isVisible(item) { 47 return item && item.status !== "hidden"; 48 } 49 50 function hasUrl(value) { 51 return Boolean(normalizeLink(value).url); 52 } 53 54 function externalAttrs(url) { 55 return /^https?:\/\//.test(normalizeLink(url).url || "") ? ' target="_blank" rel="noopener"' : ""; 56 } 57 58 function normalizeLink(value) { 59 const raw = String(value || "").trim(); 60 if (!raw || raw === "#") { 61 return { url: "", label: "" }; 62 } 63 64 const markdownMatch = raw.match(/^\[([^\]]+)\]\(([^)]+)\)$/); 65 if (markdownMatch) { 66 return { 67 label: markdownMatch[1], 68 url: markdownMatch[2].trim() 69 }; 70 } 71 72 return { url: raw, label: "" }; 73 } 74 75 function linkHtml(url, label, className) { 76 const link = normalizeLink(url); 77 if (!link.url) { 78 return ""; 79 } 80 const classAttr = className ? ` class="${escapeAttr(className)}"` : ""; 81 return `<a${classAttr} href="${escapeAttr(link.url)}"${externalAttrs(link.url)}>${escapeHtml(label || link.label || link.url)}</a>`; 82 } 83 84 function renderTextBlocks(value) { 85 const blocks = Array.isArray(value) ? value : [value].filter(Boolean); 86 return blocks.map((block) => `<p>${textToHtml(block)}</p>`).join(""); 87 } 88 89 function renderHostLinks(host) { 90 const links = visibleItems(host.links).filter((link) => hasUrl(link.url)); 91 92 if (!links.length && hasUrl(host.url)) { 93 links.push({ url: host.url, label: host.linkLabel || host.name }); 94 } 95 96 if (!links.length) { 97 return ""; 98 } 99 100 return `<div class="host-links">${links.map((link) => linkHtml(link.url, link.label || link.linkLabel || link.name || host.name, "btn btn-ghost me-2 mb-2")).join("")}</div>`; 101 } 102 103 function formatLocations(locations) { 104 if (!Array.isArray(locations) || !locations.length) { 105 return "Locations to be announced"; 106 } 107 return locations.filter(Boolean).join(", "); 108 } 109 110 function visibleItems(items) { 111 return Array.isArray(items) ? items.filter(isVisible) : []; 112 } 113 114 function speakerModalId(speaker) { 115 return `modal-speaker-${safeId(speaker.id || speaker.name)}`; 116 } 117
118 function renderSocialLinks(profileLinks) { 119 const links = visibleItems(profileLinks).filter((link) => hasUrl(link.url)); 120 if (!links.length) { 121 return ""; 122 } 123 124 const items = links.map((link) => { 125 const normalized = normalizeLink(link.url); 126 const iconClass = socialIcons[link.type] || "fa fa-link"; 127 const label = link.label || normalized.label || link.type || "Profile"; 128 return `<li class="list-inline-item"><a href="${escapeAttr(normalized.url)}"${externalAttrs(normalized.url)} aria-label="${escapeAttr(label)}"><i class="${escapeAttr(iconClass)} fa-fw"></i></a></li>`; 129 }).join(""); 130 131 return `<ul class="social-list list-inline mb-0">${items}</ul>`; 132 } 133 134 function renderPeopleLinks(people) { 135 const visiblePeople = visibleItems(people); 136 if (!visiblePeople.length) { 137 return ""; 138 } 139 140 return visiblePeople.map((person) => { 141 if (typeof person === "string") { 142 return escapeHtml(person); 143 } 144 const normalized = normalizeLink(person.url); 145 return normalized.url 146 ? `<a href="${escapeAttr(normalized.url)}" class="d-block d-md-inline-block"${externalAttrs(normalized.url)}>${escapeHtml(person.name || normalized.label || normalized.url)}</a>` 147 : escapeHtml(person.name); 148 }).join(" and "); 149 } 150 151 function renderHero(tour) { 152 const hero = byId("tour-hero-content"); 153 const heroBlock = byId("hero-block"); 154 if (!hero) { 155 return; 156 } 157 158 if (tour.heroImage && heroBlock) { 159 heroBlock.style.backgroundImage = `linear-gradient(rgba(0, 0, 0, 0.5), rgba(0,0, 0, 0.1)), url("${tour.heroImage}")`; 160 } 161 162 document.title = `${tour.title} | REStud North-American Tours`; 163 const description = document.querySelector('meta[name="description"]'); 164 if (description) { 165 description.setAttribute("content", `${tour.title}: ${tour.dates.display}; ${formatLocations(tour.locations)}`); 166 } 167 168 const dateLine = tour.dates && tour.dates.display ? tour.dates.display : "Dates to be announced"; 169 const locationLine = formatLocations(tour.locations); 170 const application = tour.application || {}; 171 const applicationButton = hasUrl(application.url) 172 ? `<div class="mt-4">${linkHtml(application.url, application.label || "Apply", "btn btn-primary btn-lg")}</div>` 173 : ""; 174 175 hero.innerHTML = ` 176 <h1 class="hero-heading mb-2">${escapeHtml(tour.title)}</h1> 177 <div class="hero-meta mb-3"><i class="far fa-calendar-alt me-2"></i>${escapeHtml(dateLine)}</div> 178 <div class="hero-meta mb-3"><i class="fas fa-map-marker-alt mx-2"></i>${escapeHtml(locationLine)}</div> 179 ${applicationButton} 180 `; 181 } 182 183 function renderHosts(tour) { 184 const heading = byId("hosts-heading"); 185 const container = byId("tour-hosts"); 186 if (!container) { 187 return; 188 } 189 190 if (heading) { 191 heading.textContent = `Hosts (${tour.editionYear})`; 192 } 193 194 const hosts = visibleItems(tour.hostInstitutions); 195 if (!hosts.length) { 196 container.innerHTML = '<p class="section-intro text-center single-col-max mx-auto mb-0">Host institutions to be announced.</p>'; 197 return; 198 } 199 200 container.innerHTML = hosts.map((host, index) => { 201 const figureClass = `figure-${(index % 4) + 1}`; 202 const localHosts = renderPeopleLinks(host.localHosts); 203 const hostLinks = renderHostLinks(host); 204 const imageHtml = host.image ? ` 205 <div class="col-5 col-lg-3 col-md-4 offset-1 h-100 d-flex align-items-center justify-content-center"> 206 <div class="figure ${figureClass}"><img class="img-fluid d-none d-md-block" src="${escapeAttr(host.image)}" alt="${escapeAttr(host.alt || host.name)}"></div> 207 </div> 208 ` : ""; 209 210 return ` 211 <div class="row"> 212 <div class="${host.image ? "col-12 col-md-7 col-lg-8" : "col-12"} h-100 d-flex flex-column justify-content-between"> 213 <div class="desc h-100 align-items-center"> 214 <h4 class="text-white mb-4 mt-3 mt-lg-5">${escapeHtml(host.name)}</h4> 215 ${renderTextBlocks(host.description)} 216 ${hostLinks} 217 ${localHosts ? `<p class="mt-3 mb-0"><strong>Local hosts:</strong> ${localHosts}</p>` : ""} 218 </div> 219 <br> 220 <br> 221 </div> 222 ${imageHtml} 223 </div> 224 `; 225 }).join(""); 226 } 227
228 function renderSpeakers(tour) { 229 const heading = byId("speakers-heading"); 230 const container = byId("tour-speakers"); 231 const modals = byId("speaker-modals"); 232 if (!container) { 233 return; 234 } 235 236 if (heading) { 237 heading.textContent = `Tourists (${tour.editionYear})`; 238 } 239 240 const speakers = visibleItems(tour.speakers); 241 if (!speakers.length) { 242 container.innerHTML = '<div class="col-12"><p class="section-intro text-center single-col-max mx-auto mb-0">Tourists to be announced.</p></div>'; 243 if (modals) { 244 modals.innerHTML = ""; 245 } 246 return; 247 } 248 249 container.innerHTML = speakers.map((speaker) => { 250 const modalId = speakerModalId(speaker); 251 const image = speaker.image 252 ? `<img src="${escapeAttr(speaker.image)}" class="card-img-top rounded-0" alt="${escapeAttr(speaker.name)}">` 253 : ""; 254 const imageLink = image 255 ? `<a href="#${escapeAttr(modalId)}" data-bs-toggle="modal" data-bs-target="#${escapeAttr(modalId)}">${image}</a>` 256 : ""; 257 const social = renderSocialLinks(speaker.profileLinks); 258 const website = linkHtml(speaker.website, "Personal Website â"); 259 const jmp = speaker.jmp && hasUrl(speaker.jmp.url) 260 ? linkHtml(speaker.jmp.url, "JMP â") 261 : ""; 262 263 return ` 264 <div class="col-xs-12 col-sm-6 col-lg-4 mb-4"> 265 <div class="card rounded-0"> 266 ${imageLink} 267 <div class="card-body"> 268 <h5 class="card-title mb-2">${escapeHtml(speaker.name)}</h5> 269 ${speaker.affiliation ? `<div class="card-text mb-3"><div class="meta">${escapeHtml(speaker.affiliation)}</div></div>` : ""} 270 ${website} 271 ${website && jmp ? "<br>" : ""} 272 ${jmp} 273 </div> 274 ${social ? `<div class="card-footer text-muted">${social}</div>` : ""} 275 </div> 276 </div> 277 `; 278 }).join(""); 279 280 if (modals) { 281 modals.innerHTML = speakers.map(renderSpeakerModal).join(""); 282 } 283 } 284
285 function renderSpeakerModal(speaker) { 286 const modalId = speakerModalId(speaker); 287 const social = renderSocialLinks(speaker.profileLinks); 288 const jmpTitle = speaker.jmp && speaker.jmp.title ? speaker.jmp.title : ""; 289 const jmpLine = jmpTitle 290 ? `<p><strong>JMP:</strong> ${hasUrl(speaker.jmp.url) ? linkHtml(speaker.jmp.url, jmpTitle) : escapeHtml(jmpTitle)}</p>` 291 : ""; 292 const websiteLine = hasUrl(speaker.website) 293 ? `<p>${linkHtml(speaker.website, "Personal Website")}</p>` 294 : ""; 295 296 return ` 297 <div class="modal modal-speaker" id="${escapeAttr(modalId)}" tabindex="-1" role="dialog" aria-labelledby="${escapeAttr(modalId)}-label" aria-hidden="true"> 298 <div class="modal-dialog"> 299 <div class="modal-content"> 300 <div class="modal-header"> 301 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true">×</button> 302 <h4 id="${escapeAttr(modalId)}-label" class="modal-title sr-only">${escapeHtml(speaker.name)}</h4> 303 </div> 304 <div class="modal-body p-0"> 305 <div class="theme-bg-light p-5"> 306 <div class="row"> 307 ${speaker.image ? `<div class="col-12 col-md-auto text-center"><img class="profile-image mb-3 mb-md-0 me-md-4 rounded-circle mx-auto" src="${escapeAttr(speaker.image)}" alt="${escapeAttr(speaker.name)}"></div>` : ""} 308 <div class="col text-center text-md-start mx-auto"> 309 <h2 class="name mb-2">${escapeHtml(speaker.name)}</h2> 310 ${speaker.affiliation ? `<div class="meta mb-2">${escapeHtml(speaker.affiliation)}</div>` : ""} 311 ${social} 312 </div> 313 </div> 314 </div> 315 <div class="desc p-4 p-lg-5"> 316 ${speaker.bio ? `<p>${textToHtml(speaker.bio)}</p>` : ""} 317 ${jmpLine} 318 ${websiteLine} 319 </div> 320 </div> 321 </div> 322 </div> 323 </div> 324 `; 325 } 326
327 function renderSchedule(tour) { 328 const heading = byId("schedule-heading"); 329 const container = byId("tour-schedule"); 330 if (!container) { 331 return; 332 } 333 334 if (heading) { 335 heading.textContent = `Tour Agenda (${tour.editionYear})`; 336 } 337 338 const stops = visibleItems(tour.schedule); 339 const speakersById = {}; 340 visibleItems(tour.speakers).forEach((speaker) => { 341 speakersById[speaker.id] = speaker; 342 }); 343 344 if (!stops.length) { 345 container.innerHTML = '<p class="section-intro text-center single-col-max mx-auto mb-0">Agenda to be announced.</p>'; 346 return; 347 } 348 349 const nav = stops.map((stop, index) => { 350 const tabId = `tab-${safeId(stop.id || stop.host || index + 1)}`; 351 return ` 352 <li class="nav-item ${index < stops.length - 1 ? "me-2" : ""}"> 353 <a class="nav-link ${index === 0 ? "active" : ""}" id="${escapeAttr(tabId)}" data-bs-toggle="tab" href="#${escapeAttr(tabId)}-content" role="tab" aria-controls="${escapeAttr(tabId)}-content" aria-selected="${index === 0 ? "true" : "false"}"> 354 <span class="heading">${escapeHtml(stop.host)}</span> 355 ${stop.date ? `<span class="meta">(${escapeHtml(stop.date)})</span>` : ""} 356 </a> 357 </li> 358 `; 359 }).join(""); 360 361 const panes = stops.map((stop, index) => renderSchedulePane(stop, index, speakersById)).join(""); 362 363 container.innerHTML = ` 364 <ul class="schedule-nav nav nav-pills nav-fill" id="tour-schedule-tabs" role="tablist">${nav}</ul> 365 <div class="schedule-tab-content tab-content">${panes}</div> 366 `; 367 } 368
369 function renderSchedulePane(stop, index, speakersById) { 370 const tabId = `tab-${safeId(stop.id || stop.host || index + 1)}`; 371 const items = visibleItems(stop.items); 372 const noTimeline = stop.noTimeline || !items.some((item) => item.type === "talk"); 373 const localHosts = renderPeopleLinks(stop.localHosts); 374 375 const agenda = items.length 376 ? items.map((item) => renderScheduleItem(item, speakersById)).join("") 377 : '<h4 class="text-center py-5 text-muted">Agenda to be announced.</h4>'; 378 379 return ` 380 <div class="tab-pane ${index === 0 ? "active" : ""} ${noTimeline ? "no-timeline" : ""}" id="${escapeAttr(tabId)}-content" role="tabpanel" aria-labelledby="${escapeAttr(tabId)}"> 381 ${agenda} 382 ${localHosts ? `<br><div><h6>Local Organizers: ${localHosts}</h6></div><br>` : ""} 383 </div> 384 `; 385 } 386 387 function renderScheduleItem(item, speakersById) { 388 if (item.type === "day") { 389 return renderScheduleDay(item); 390 } 391 392 const speaker = item.speakerId ? speakersById[item.speakerId] : null; 393 const title = item.title || (speaker && speaker.jmp && speaker.jmp.title) || "Talk"; 394 const titleUrl = item.url || item.link || item.paperUrl || (speaker && speaker.jmp && speaker.jmp.url) || ""; 395 const titleHtml = hasUrl(titleUrl) 396 ? linkHtml(titleUrl, title, "theme-link") 397 : escapeHtml(title); 398 const itemClass = item.type === "talk" ? "item-talk" : "item-other"; 399 const profile = speaker ? renderScheduleProfile(speaker) : ""; 400 const descriptions = Array.isArray(item.description) ? item.description : [item.description].filter(Boolean); 401 const discussant = item.discussant ? `<div class="desc">Discussant: ${textToHtml(item.discussant)}</div>` : ""; 402 403 return ` 404 <div class="item ${itemClass}"> 405 <div class="meta"> 406 ${item.time ? `<h4 class="time mb-3">${escapeHtml(item.time)}</h4>` : ""} 407 ${profile} 408 </div> 409 <div class="content"> 410 <h3 class="title mb-3">${titleHtml}</h3> 411 ${item.location ? `<div class="location mb-3"><i class="fas fa-map-marker-alt me-2"></i>${escapeHtml(item.location)}</div>` : ""} 412 ${descriptions.map((description) => `<div class="desc">${textToHtml(description)}</div>`).join("")} 413 ${discussant} 414 </div> 415 </div> 416 `; 417 } 418
419 function renderScheduleDay(item) { 420 const title = item.title || item.date || "Schedule Day"; 421 const descriptions = Array.isArray(item.description) ? item.description : [item.description].filter(Boolean); 422 423 return ` 424 <div class="item item-day"> 425 <div class="day-content"> 426 <h3 class="day-heading">${escapeHtml(title)}</h3> 427 ${descriptions.map((description) => `<div class="day-desc">${textToHtml(description)}</div>`).join("")} 428 </div> 429 </div> 430 `; 431 } 432 433 function renderScheduleProfile(speaker) { 434 const modalId = speakerModalId(speaker); 435 const image = speaker.image 436 ? `<a href="#${escapeAttr(modalId)}" data-bs-toggle="modal" data-bs-target="#${escapeAttr(modalId)}"><img class="profile-image rounded-circle mb-2" src="${escapeAttr(speaker.image)}" alt="${escapeAttr(speaker.name)}"></a>` 437 : ""; 438 return ` 439 <div class="profile"> 440 ${image} 441 <div class="name"><a class="theme-link" href="#${escapeAttr(modalId)}" data-bs-toggle="modal" data-bs-target="#${escapeAttr(modalId)}">${escapeHtml(speaker.name)}</a></div> 442 </div> 443 `; 444 } 445 446 function renderPastTours() { 447 const container = byId("past-tours-carousel"); 448 if (!container) { 449 return; 450 } 451 452 const tours = visibleItems(data.pastTours); 453 if (!tours.length) { 454 container.innerHTML = ""; 455 return; 456 } 457 458 container.innerHTML = tours.map((tour, index) => { 459 const position = tour.imagePosition || "center center"; 460 const locations = formatLocations(tour.locations); 461 return ` 462 <div class="carousel-item ${index === 0 ? "active" : ""}"> 463 <div class="d-flex align-items-center justify-content-center text-center" style="height: 70vh; background: linear-gradient(rgba(0, 0, 0, 0.1), rgba(0,0, 0, 0.6)), url('${escapeAttr(tour.image)}') no-repeat ${escapeAttr(position)}; background-size: cover;"> 464 <a href="${escapeAttr(tour.url)}" class="text-white text-decoration-none"${externalAttrs(tour.url)}> 465 <h1 class="text-center text-white">${escapeHtml(tour.title)}</h1> 466 <div class="hero-meta mb-2">${escapeHtml(tour.dates || "")}</div> 467 <div>${escapeHtml(locations)}</div> 468 </a> 469 </div> 470 </div> 471 `; 472 }).join(""); 473 } 474
475 function renderSite() { 476 const tour = data.upcomingTour; 477 renderHero(tour); 478 renderHosts(tour); 479 renderSpeakers(tour); 480 renderSchedule(tour); 481 renderPastTours(); 482 } 483 484 window.RESTUD_TOUR_RENDERER = { render: renderSite }; 485 renderSite(); 486})();
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.