1(function () { 2 'use strict'; 3 4 var config = {}; 5 var currentPage = -1; 6 7 // ââ Parse config.md ââââââââââââââââââââââââââââââ 8 function loadConfig() { 9 return fetch('config.md', { cache: 'no-cache' }) 10 .then(function (r) { return r.text(); }) 11 .then(function (text) { 12 var lines = text.split('\n'); 13 var section = null; 14 config = { site: {}, menu: [], footer: [], protectedHash: null }; 15 16 for (var i = 0; i < lines.length; i++) { 17 var trimmed = lines[i].trim(); 18 if (trimmed.indexOf('## ') === 0) { 19 section = trimmed.slice(3).trim().toLowerCase(); 20 } else if (trimmed.indexOf('- ') === 0 && section) { 21 var content = trimmed.slice(2); 22 if (section === 'site') { 23 var colonIdx = content.indexOf(':'); 24 if (colonIdx > -1) { 25 var key = content.slice(0, colonIdx).trim(); 26 var val = content.slice(colonIdx + 1).trim(); 27 config.site[key] = val; 28 } 29 } else if (section === 'menu') { 30 var parts = content.split('|'); 31 if (parts.length >= 3) { 32 config.menu.push({ 33 label: parts[0].trim(), 34 icon: parts[1].trim(), 35 source: parts[2].trim() 36 }); 37 } 38 } else if (section === 'footer') { 39 var parts2 = content.split('|'); 40 if (parts2.length >= 2) { 41 config.footer.push({ 42 label: parts2[0].trim(), 43 url: parts2[1].trim() 44 }); 45 } 46 } else if (section === 'copyright') { 47 var parts3 = content.split('|'); 48 if (parts3.length >= 2) { 49 config.site['copyright'] = config.site['copyright'] ? config.site['copyright'] + ' ' + parts3[1].trim() : parts3[1].trim(); 50 } 51 } else if (section === 'protected') { 52 var colonIdx2 = content.indexOf(':'); 53 if (colonIdx2 > -1) { 54 var pkey = content.slice(0, colonIdx2).trim(); 55 var pval = content.slice(colonIdx2 + 1).trim(); 56 if (pkey === 'hash') config.protectedHash = pval; 57 } 58 } 59 } 60 } 61 return config; 62 }); 63 } 64 65 // ââ Build UI âââââââââââââââââââââââââââââââââââââ 66 function buildUI() { 67 var siteName = config.site.title || 'Academic Profile'; 68 var nameParts = siteName.split(' '); 69 var brandHTML = nameParts.length > 1 70 ? nameParts.slice(0, -1).join(' ') + ' <span class="brand-accent">' + nameParts[nameParts.length - 1] + '</span>' 71 : siteName; 72 73 // Header brand 74 var titleEl = document.getElementById('site-title'); 75 titleEl.innerHTML = brandHTML; 76 titleEl.addEventListener('click', function () { navigateTo(0); }); 77 78 document.title = siteName; 79 80 // Nav links 81 var menu = document.getElementById('nav-menu'); 82 for (var i = 0; i < config.menu.length; i++) { 83 (function (index) { 84 var item = config.menu[index]; 85 var a = document.createElement('a'); 86 a.href = '/' + slugify(item.label); 87 a.textContent = item.label; 88 a.addEventListener('click', function (e) { 89 e.preventDefault(); 90 navigateTo(index); 91 closeMobileMenu(); 92 }); 93 menu.appendChild(a); 94 })(i); 95 } 96 97 // Footer 98 var footerTitle = document.getElementById('footer-title');
99 footerTitle.innerHTML = brandHTML; 100 101 var footerCopy = document.getElementById('footer-copy'); 102 footerCopy.textContent = '\u00A9 ' + (config.site.copyright || new Date().getFullYear()); 103 104 var footerLinks = document.getElementById('footer-links'); 105 for (var j = 0; j < config.footer.length; j++) { 106 var fl = config.footer[j]; 107 var fa = document.createElement('a'); 108 fa.href = fl.url; 109 fa.target = '_blank'; 110 fa.rel = 'noopener'; 111 fa.textContent = fl.label; 112 footerLinks.appendChild(fa); 113 } 114 } 115 116 // ââ Analytics (SPA page views) âââââââââââââââââââ 117 function trackPageView(pageTitle) { 118 var siteName = config.site.title || 'Academic Profile'; 119 var fullTitle = pageTitle ? pageTitle + ' | ' + siteName : siteName; 120 document.title = fullTitle; 121 122 if (typeof gtag === 'function') { 123 gtag('event', 'page_view', { 124 page_title: fullTitle, 125 page_location: window.location.href, 126 page_path: window.location.pathname 127 }); 128 } 129 } 130 131 function pageTitleFromMd(md) { 132 var m = md.match(/^#{1,6}\s+(.+)$/m); 133 if (!m) return null; 134 return m[1] 135 .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') 136 .replace(/[*_`#]/g, '') 137 .trim(); 138 } 139 140 // ââ Navigation âââââââââââââââââââââââââââââââââââ 141 function navigateTo(index, skipHistory) { 142 if (currentPage === index) return; 143 currentPage = index; 144 145 var links = document.querySelectorAll('#nav-menu a'); 146 for (var i = 0; i < links.length; i++) { 147 if (i === index) { 148 links[i].classList.add('active'); 149 } else { 150 links[i].classList.remove('active'); 151 } 152 } 153 154 var contentEl = document.getElementById('page-content'); 155 contentEl.innerHTML = '<div class="loading"><div class="loading-spinner"></div></div>';
156 contentEl.style.animation = 'none'; 157 contentEl.offsetHeight; 158 contentEl.style.animation = ''; 159 160 var item = config.menu[index]; 161 162 var promise; 163 if (item.source.charAt(item.source.length - 1) === '/') { 164 promise = loadBibFolder(item.source); 165 } else if (endsWith(item.source, '.bib')) { 166 promise = fetch(item.source, { cache: 'no-cache' }) 167 .then(function (r) { return r.text(); }) 168 .then(function (bib) { renderPublications(parseBib(bib)); }); 169 } else { 170 promise = fetch(item.source, { cache: 'no-cache' }) 171 .then(function (r) { return r.text(); }) 172 .then(function (md) { renderMarkdown(md); }); 173 } 174 175 promise.catch(function (err) { 176 contentEl.innerHTML = '<p style="color:var(--secondary);">Erro ao carregar conteúdo.</p>'; 177 console.error(err); 178 }); 179 180 var url = '/' + slugify(item.label); 181 if (skipHistory) { 182 history.replaceState({ type: 'menu', index: index }, '', url); 183 } else { 184 history.pushState({ type: 'menu', index: index }, '', url); 185 } 186 trackPageView(item.label); 187 window.scrollTo({ top: 0, behavior: 'smooth' }); 188 } 189 190 // ââ Load bib folder ââââââââââââââââââââââââââââââ 191 var bibSources = {}; 192 var bibFolder = ''; 193 194 function loadBibFolder(folder) { 195 bibFolder = folder; 196 return fetch(folder + '_index.md', { cache: 'no-cache' }) 197 .then(function (r) { return r.text(); }) 198 .then(function (indexText) { 199 var files = []; 200 var lines = indexText.split('\n'); 201 var section = null; 202 bibSources = {}; 203 204 for (var i = 0; i < lines.length; i++) { 205 var trimmed = lines[i].trim(); 206 if (trimmed.indexOf('## ') === 0) { 207 section = trimmed.slice(3).trim().toLowerCase(); 208 continue; 209 } 210 if (trimmed.indexOf('- ') === 0) { 211 var content = trimmed.slice(2); 212 if (!section) { 213 var bibMatch = content.match(/^(.+\.bib)/); 214 if (bibMatch) files.push(bibMatch[1]); 215 } else if (section === 'sources') { 216 var parts = content.split('|'); 217 if (parts.length >= 2) { 218 bibSources[parts[0].trim()] = parts[1].trim(); 219 } 220 } 221 } 222 } 223 224 var promises = files.map(function (file) { 225 return fetch(folder + file, { cache: 'no-cache' }).then(function (r) { return r.text(); }); 226 }); 227 228 return Promise.all(promises); 229 }) 230 .then(function (results) { 231 var allEntries = []; 232 for (var i = 0; i < results.length; i++) { 233 allEntries = allEntries.concat(parseBib(results[i])); 234 } 235 return discoverAssets(allEntries); 236 }) 237 .then(function (entries) { 238 renderPublications(entries); 239 }); 240 } 241 242 // ââ Autodiscover PDF & Slides ââââââââââââââââââââ 243 function discoverAssets(entries) { 244 var checks = []; 245 246 for (var i = 0; i < entries.length; i++) { 247 (function (entry) { 248 var key = entry.key; 249 250 var pdfCheck = fetch(bibFolder + 'pdf/' + key + '.pdf', { method: 'HEAD' }) 251 .then(function (r) { 252 if (r.ok) entry._pdf = bibFolder + 'pdf/' + key + '.pdf'; 253 }) 254 .catch(function () {}); 255 256 var slidesCheck = fetch(bibFolder + 'slides/' + key + '.pdf', { method: 'HEAD' }) 257 .then(function (r) {
258 if (r.ok) entry._slides = bibFolder + 'slides/' + key + '.pdf'; 259 }) 260 .catch(function () {}); 261 262 checks.push(pdfCheck); 263 checks.push(slidesCheck); 264 })(entries[i]); 265 } 266 267 return Promise.all(checks).then(function () { return entries; }); 268 } 269 270 // ââ BibTeX parser ââââââââââââââââââââââââââââââââ 271 function parseBib(text) { 272 var entries = []; 273 var i = 0; 274 275 while (i < text.length) { 276 var atIdx = text.indexOf('@', i); 277 if (atIdx === -1) break; 278 279 var braceIdx = text.indexOf('{', atIdx); 280 if (braceIdx === -1) break; 281 282 var type = text.substring(atIdx + 1, braceIdx).trim().toLowerCase(); 283 284 var commaIdx = text.indexOf(',', braceIdx); 285 if (commaIdx === -1) break; 286 287 var key = text.substring(braceIdx + 1, commaIdx).trim(); 288 289 var depth = 1; 290 var j = braceIdx + 1; 291 while (j < text.length && depth > 0) { 292 if (text.charAt(j) === '{') depth++; 293 else if (text.charAt(j) === '}') depth--; 294 j++; 295 } 296 297 var body = text.substring(commaIdx + 1, j - 1); 298 299 var entry = { type: type, key: key, fields: {} }; 300 var fieldRegex = /(\w+)\s*=\s*\{((?:[^{}]|\{[^{}]*\})*)\}/g; 301 var fieldMatch; 302 while ((fieldMatch = fieldRegex.exec(body)) !== null) { 303 entry.fields[fieldMatch[1].toLowerCase()] = fieldMatch[2].trim(); 304 } 305 306 if (type !== 'comment' && type !== 'string' && type !== 'preamble') { 307 entries.push(entry); 308 } 309 310 i = j; 311 } 312 313 return entries; 314 } 315 316 // ââ Render publications ââââââââââââââââââââââââââ 317 function renderPublications(entries) { 318 var byYear = {}; 319 for (var i = 0; i < entries.length; i++) { 320 var year = entries[i].fields.year || 'S/D'; 321 if (!byYear[year]) byYear[year] = []; 322 byYear[year].push(entries[i]); 323 } 324 325 var years = Object.keys(byYear).sort(function (a, b) { return b - a; }); 326 327 var html = '<div class="pub-header">'; 328 html += '<h1>Publications</h1>'; 329 html += '<p class="pub-stats"><strong>' + entries.length + '</strong> entries</p>'; 330 html += '</div>'; 331 332 for (var y = 0; y < years.length; y++) { 333 var yr = years[y]; 334 var yearEntries = byYear[yr]; 335 336 html += '<div class="pub-year-group">'; 337 html += '<div class="pub-year-title">' + yr + ' <span class="pub-count">' + yearEntries.length + '</span></div>'; 338 339 for (var e = 0; e < yearEntries.length; e++) { 340 var entry = yearEntries[e]; 341 var f = entry.fields; 342 var title = f.title || 'Sem t\u00edtulo'; 343 var authors = f.author || ''; 344 var venue = f.booktitle || f.journal || f.school || ''; 345 var note = f.note 346 ? '<span class="pub-note"><span class="material-symbols-outlined" style="font-size:14px">emoji_events</span> ' + f.note + '</span>' 347 : ''; 348 var typeLabel = getTypeLabel(entry.type); 349 var venueExtra = ''; 350 if (venue) { 351 venueExtra = venue; 352 if (f.volume) venueExtra += ', vol. ' + f.volume; 353 if (f.pages) venueExtra += ', pp. ' + f.pages; 354 } 355 356 html += '<div class="pub-entry">'; 357 html += '<div class="pub-title">' + title + '</div>'; 358 html += '<div class="pub-authors">' + formatAuthors(authors) + '</div>'; 359 if (venueExtra) html += '<div class="pub-venue">' + venueExtra + '</div>'; 360 html += '<div class="pub-meta">'; 361 html += '<span class="pub-type ' + entry.type + '">' + typeLabel + '</span>'; 362 html += note; 363 html += '</div>'; 364 365 // Action links row 366 var actions = []; 367 if (entry._pdf) { 368 actions.push('<a href="' + entry._pdf + '" target="_blank" class="pub-action pub-action-pdf"><span class="material-symbols-outlined">picture_as_pdf</span> PDF</a>'); 369 } 370 if (entry._slides) { 371 actions.push('<a href="' + entry._slides + '" target="_blank" class="pub-action pub-action-sli
371des"><span class="material-symbols-outlined">slideshow</span> Slides</a>'); 372 } 373 if (f.doi) { 374 actions.push('<a href="https://doi.org/' + f.doi + '" target="_blank" class="pub-action pub-action-doi"><span class="material-symbols-outlined">link</span> DOI</a>'); 375 } else if (f.url) { 376 actions.push('<a href="' + f.url + '" target="_blank" class="pub-action pub-action-url"><span class="material-symbols-outlined">open_in_new</span> URL</a>'); 377 } 378 if (bibSources[entry.key]) { 379 actions.push('<a href="' + bibSources[entry.key] + '" target="_blank" class="pub-action pub-action-github"><span class="material-symbols-outlined">code</span> Code</a>'); 380 } 381 if (actions.length > 0) { 382 html += '<div class="pub-actions">' + actions.join('') + '</div>'; 383 } 384 385 html += '</div>'; 386 } 387 388 html += '</div>'; 389 } 390 391 document.getElementById('page-content').innerHTML = html; 392 } 393 394 function getTypeLabel(type) { 395 var labels = { 396 article: 'Journal', 397 inproceedings: 'Conference', 398 incollection: 'Chapter', 399 mastersthesis: 'Thesis', 400 phdthesis: 'PhD Thesis', 401 preprint: 'Preprint', 402 misc: 'Other' 403 }; 404 return labels[type] || type; 405 } 406 407 function formatAuthors(authors) { 408 return authors 409 .replace(/\s+and\s+/g, '; ') 410 .replace(/\s+/g, ' ') 411 .trim(); 412 } 413 414 // ââ Render markdown ââââââââââââââââââââââââââââââ 415 function renderMarkdown(md) { 416 var contentEl = document.getElementById('page-content'); 417 contentEl.innerHTML = marked.parse(md); 418 bindInternalLinks(contentEl); 419 } 420 421 // ââ Auth session (in-memory only) ââââââââââââââââ 422 var authUnlocked = false; 423 424 function sha256(message) { 425 var msgBuffer = new TextEncoder().encode(message); 426 return crypto.subtle.digest('SHA-256', msgBuffer).then(function (hashBuffer) { 427 var hashArray = Array.from(new Uint8Array(hashBuffer)); 428 return hashArray.map(function (b) { return b.toString(16).padStart(2, '0'); }).join(''); 429 }); 430 } 431 432 function isProtectedPath(src) { 433 return src.indexOf('pages/p/') !== -1; 434 } 435 436 function showAuthModal(onSuccess) { 437 var modal = document.getElementById('auth-modal'); 438 var input = document.getElementById('auth-password'); 439 var btn = document.getElementById('auth-submit'); 440 var err = document.getElementById('auth-error'); 441 442 modal.hidden = false; 443 err.hidden = true; 444 input.value = ''; 445 setTimeout(function () { input.focus(); }, 100); 446 447 function attempt() { 448 var pw = input.value; 449 if (!pw) return; 450 sha256(pw).then(function (hash) { 451 if (hash === config.protectedHash) { 452 authUnlocked = true; 453 modal.hidden = true; 454 onSuccess(); 455 } else { 456 err.hidden = false; 457 input.value = ''; 458 input.focus(); 459 } 460 }); 461 } 462 463 btn.onclick = attempt; 464 input.onkeydown = function (e) { if (e.key === 'Enter') attempt(); }; 465 } 466 467 function loadPage(src, skipHistory) { 468 if (isProtectedPath(src) && !authUnlocked) { 469 if (!config.protectedHash) { 470 document.getElementById('page-content').innerHTML = '<p style="color:var(--secondary);">Acesso negado.</p>'; 471 return; 472 } 473 showAuthModal(function () { loadPage(src, skipHistory); }); 474 return; 475 } 476 477 var contentEl = document.getElementById('page-content'); 478 contentEl.innerHTML = '<div class="loading"><div class="loading-spinner"></div></div>'; 479 currentPage = -1; 480 var links = document.querySelectorAll('#nav-menu a'); 481 for (var i = 0; i < links.length; i++) links[i].classList.remove('active'); 482 fetch(src, { cache: 'no-cache' }) 483 .then(function (r) { 484 if (!r.ok) throw new Error('403'); 485 return r.text(); 486 }) 487 .then(function (md) { 488 renderMarkdown(md); 489 trackPageView(pageTitleFromMd(md)); 490 }) 491 .catch(function () { 492 contentEl.innerHTML = '<p style="color:var(--secondary);">Erro ao carregar conteúdo.</p>'; 493 }); 494 if (!skipHistory) { 495 history.pushState({ type: 'page', src: src }, '', '/' + src.replace(/\.md$/, '')); 496 } 497 window.scrollTo({ top: 0, behavior: 'smooth' }); 498 } 499 500 function bindInternalLinks(container) { 501 var links = container.querySelectorAll('a[href]'); 502 for (var i = 0; i < links.length; i++) { 503 (function (link) { 504 var href = link.getAttribute('href'); 505 if (endsWith(href, '.md') && href.indexOf('://') === -1) { 506 var src = href.replace(/^\//, ''); 507 link.setAttribute('href', '/' + src.replace(/\.md$/, '')); 508 link.addEventListener('click', function (e) { 509 e.preventDefault(); 510 loadPage(src); 511 }); 512 } 513 })(links[i]); 514 } 515 } 516 517 // ââ Mobile menu ââââââââââââââââââââââââââââââââââ 518 function setupMobileMenu() { 519 var toggle = document.getElementById('menu-toggle'); 520 var navLinks = document.getElementById('nav-menu');
521 var overlay = document.getElementById('mobile-overlay'); 522 523 if (toggle) { 524 toggle.addEventListener('click', function () { 525 navLinks.classList.toggle('open'); 526 overlay.classList.toggle('active'); 527 }); 528 } 529 530 if (overlay) { 531 overlay.addEventListener('click', function () { 532 closeMobileMenu(); 533 }); 534 } 535 } 536 537 function closeMobileMenu() { 538 var navLinks = document.getElementById('nav-menu'); 539 var overlay = document.getElementById('mobile-overlay'); 540 if (navLinks) navLinks.classList.remove('open'); 541 if (overlay) overlay.classList.remove('active'); 542 } 543 544 // ââ Utilities ââââââââââââââââââââââââââââââââââââ 545 function slugify(str) { 546 return str.toLowerCase() 547 .normalize('NFD') 548 .replace(/[\u0300-\u036f]/g, '') 549 .replace(/\s+/g, '-') 550 .replace(/[^a-z0-9-]/g, ''); 551 } 552 553 function endsWith(str, suffix) { 554 return str.indexOf(suffix, str.length - suffix.length) !== -1; 555 } 556 557 // ââ History ââââââââââââââââââââââââââââââââââââââââ 558 window.addEventListener('popstate', function (e) { 559 var state = e.state; 560 if (state && state.type === 'page') { 561 loadPage(state.src, true); 562 } else if (state && state.type === 'menu') { 563 currentPage = -1; 564 navigateTo(state.index, true); 565 } else { 566 resolveRoute(); 567 } 568 }); 569 570 function resolveRoute() { 571 // Compatibilidade: redireciona URLs antigas com #hash para paths reais 572 var hash = window.location.hash.slice(1); 573 if (hash) { 574 var target = hash.indexOf('page:') === 0 575 ? '/' + hash.slice(5).replace(/\.md$/, '') 576 : '/' + hash; 577 history.replaceState(null, '', target); 578 } 579 580 var path = window.location.pathname.replace(/\/+$/, ''); 581 if (path === '' || path === '/index.html') path = '/'; 582 583 currentPage = -1; 584 585 if (path === '/') { 586 navigateTo(0, true); 587 return; 588 } 589 590 var seg = decodeURIComponent(path.slice(1)); 591 592 // Página do menu (slug do label) 593 for (var i = 0; i < config.menu.length; i++) { 594 if (slugify(config.menu[i].label) === seg) { 595 navigateTo(i, true); 596 return; 597 } 598 } 599 600 // Página interna .md (URL sem extensão) 601 loadPage(seg + '.md', true); 602 } 603 604 // ââ Init âââââââââââââââââââââââââââââââââââââââââ 605 function init() { 606 setupMobileMenu(); 607 608 loadConfig().then(function () { 609 buildUI(); 610 resolveRoute(); 611 }); 612 } 613 614 if (document.readyState === 'loading') { 615 document.addEventListener('DOMContentLoaded', init); 616 } else { 617 init(); 618 } 619})();
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.