PageSourceSearch

https://tarlis.com.br/js/app.js?v=2

js tarlis.com.br collected 2026-10-06 02:44:22 UTC 24,522 bytes, 619 lines download raw bytes

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.