1/* ============================================================ 2 Publications page â render, filter, search, sort, cite. 3 Depends on window.PUBS (data.js). 4 ============================================================ */ 5(function () { 6 'use strict'; 7 var PUBS = window.PUBS || []; 8 9 var TYPE_LABELS = { 10 book: 'Book', article: 'Article', chapter: 'Chapter', 11 proceedings: 'Proceedings', essay: 'Essay / Report', 12 review: 'Book review', working: 'Working paper' 13 }; 14 var THEME_LABELS = { nonprofit: 'Nonprofit & Governance', internet: 'Internet Innovation' }; 15 16 /* ---- Name / citation formatting -------------------------- */ 17 var PARTICLES = { de:1, la:1, le:1, van:1, von:1, der:1, del:1, di:1, da:1, dos:1, du:1, bin:1, al:1 }; 18 function famGiven(name) { 19 var t = name.trim().split(/\s+/); 20 var fam = [t.pop()]; 21 while (t.length && PARTICLES[t[t.length - 1].toLowerCase()]) fam.unshift(t.pop()); 22 return { family: fam.join(' '), given: t.join(' ') }; 23 } 24 function initials(given) { 25 if (!given) return ''; 26 return given.split(/\s+/).map(function (g) { return g.charAt(0).toUpperCase() + '.'; }).join(' '); 27 } 28 function apaAuthors(names) { 29 var fmt = names.map(function (n) { var p = famGiven(n); var i = initials(p.given); return p.family + (i ? ', ' + i : ''); }); 30 if (fmt.length === 1) return fmt[0]; 31 if (fmt.length === 2) return fmt[0] + ', & ' + fmt[1]; 32 return fmt.slice(0, -1).join(', ') + ', & ' + fmt[fmt.length - 1]; 33 } 34 function bibAuthors(names) { 35 return names.map(function (n) { var p = famGiven(n); return p.family + (p.given ? ', ' + p.given : ''); }).join(' and '); 36 } 37 function venueFull(p) { return p.container + (p.detail ? ', ' + p.detail : ''); } 38 39 function apaCite(p) { 40 var yr = p.forthcoming ? p.year + ', in press' : p.year; 41 var s = apaAuthors(p.authors) + ' (' + yr + '). ' + p.title + '. ' + venueFull(p) + '.'; 42 if (p.url && /^https?:/.test(p.url)) s += ' ' + p.url; 43 return s; 44 } 45 function citeKey(p) { 46 var fam = famGiven(p.authors[0]).family.replace(/[^A-Za-z]/g, '').toLowerCase(); 47 var w = ((p.title.match(/[A-Za-z]{4,}/)) || ['ref'])[0].toLowerCase(); 48 return fam + p.year + w; 49 } 50 function bibType(t) { return ({ book: 'book', article: 'article', chapter: 'incollection', proceedings: 'inproceedings' })[t] || 'misc'; } 51 function bibCite(p) { 52 var f = []; 53 f.push('author = {' + bibAuthors(p.authors) + '}'); 54 f.push('title = {' + p.title + '}'); 55 if (p.type === 'article') f.push('journal = {' + p.container + '}'); 56 else if (p.type === 'chapter' || p.type === 'proceedings') f.push('booktitle = {' + p.container + '}'); 57 else if (p.type === 'book') f.push('publisher = {' + p.container + '}'); 58 else f.push('howpublished = {' + p.container + '}'); 59 f.push('year = {' + p.year + '}'); 60 var doiMatch = p.url && p.url.match(/doi\.org\/(.+)$/); 61 if (doiMatch) f.push('doi = {' + doiMatch[1] + '}'); 62 else if (p.url) f.push('url = {' + p.url + '}'); 63 return '@' + bibType(p.type) + '{' + citeKey(p) + ',\n ' + f.join(',\n ') + '\n}'; 64 } 65 66 /* ---- Display helpers ------------------------------------- */ 67 function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); } 68 function authorLine(names) { 69 var out = names.map(function (n) { 70 return n === 'Nicole P. Marwell' ? '<strong>' + n + '</strong>' : esc(n); 71 }); 72 if (out.length === 1) return out[0]; 73 if (out.length === 2) return out[0] + ' & ' + out[1]; 74 return out.slice(0, -1).join(', ') + ', & ' + out[out.length - 1]; 75 } 76 77 /* ---- Build one publication node -------------------------- */ 78 function buildNode(p) { 79 var el = document.createElement('article'); 80 el.className = 'pub'; 81 el.setAttribute('data-type', p.type); 82 el.setAttribute('data-themes', (p.themes || []).join(' ')); 83 el.setAttribute('data-year', p.year); 84 el.setAttribute('data-text', (p.title + ' ' + p.authors.join(' ') + ' ' + p.container).toLowerCase()); 85 86 var badges = '<span class="pub-badge type">' + TYPE_LABELS[p.type] + '</span>'; 87 if (p.forthcoming) badges += '<span class="pub-badge forth">Forthcoming</span>'; 88 if (p.award) badges += '<span class="pub-badge award">' + esc(p.award) + '</span>'; 89 90 var actions = ''; 91 if (p.pdf) actions += '<a class="pub-link" href="' + p.pdf + '">PDF <span aria-hidden="true">â</span></a>'; 92 if (p.url) actions += '<a class="pub-link" href="' + p.url + '" target="_blank" rel="noopener">' + (p.urlLabel || 'Link') + ' <span aria-hidden="true">â</span></a>'; 93 if (p.abstract) actions += '<button class="pub-abstract-toggle" type="button" aria-expanded="false">Abstract</button>'; 94 actions += 95 '<span class="pub-cite">' + 96 '<button class="cite-btn" type="button" aria-haspopup="true" aria-expanded="false">Cite <span aria-hidden="true">â¾</span></button>' + 97 '<span class="cite-menu" role="menu">' + 98 '<button type="button" data-cite="apa" role="menuitem">Copy reference</button>' + 99 '<button type="button" data-cite="bibtex" role="menuitem">Copy BibTeX</button>' + 100 '</span>' + 101 '</span>'; 102 103 el.innerHTML = 104 '<div class="pub-year">' + p.year + '</div>' + 105 '<div class="pub-body">' + 106 '<h3 class="pub-title"><a href="papers/' + p.id + '.html">' + esc(p.title) + '</a></h3>' + 107 '<div class="pub-authors">' + authorLine(p.authors) + '</div>' + 108 '<div class="pub-venue">' + esc(venueFull(p)) + '</div>' + 109 '<div class="pub-badges">' + badges + '</div>' + 110 '<div class="pub-actions">' + actions + '</div>' + 111 (p.abstract ? '<div class="pub-abstract" hidden>' + esc(p.abstract) + '</div>' : '') + 112 '</div>'; 113 114 // abstract toggle 115 var atog = el.querySelector('.pub-abstract-toggle'); 116 if (atog) { 117 atog.addEventListener('click', function () { 118 var ab = el.querySelector('.pub-abstract'); 119 var open = ab.hasAttribute('hidden'); 120 if (open) { ab.removeAttribute('hidden'); atog.setAttribute('aria-expanded', 'true'); atog.textContent = 'Hide abstract'; } 121 else { ab.setAttribute('hidden', ''); atog.setAttribute('aria-expanded', 'false'); atog.textContent = 'Abstract'; } 122 }); 123 } 124 125 // cite menu 126 var citeWrap = el.querySelector('.pub-cite'); 127 var citeBtn = citeWrap.querySelector('.cite-btn'); 128 citeBtn.addEventListener('click', function (e) { 129 e.stopPropagation(); 130 var isOpen = citeWrap.classList.contains('open'); 131 closeAllCiteMenus(); 132 if (!isOpen) { citeWrap.classList.add('open'); citeBtn.setAttribute('aria-expanded', 'true'); } 133 }); 134 citeWrap.querySelectorAll('[data-cite]').forEach(function (b) { 135 b.addEventListener('click', function () { 136 var text = b.getAttribute('data-cite') === 'bibtex' ? bibCite(p) : apaCite(p); 137 copyText(text, b.getAttribute('data-cite') === 'bibtex' ? 'BibTeX copied' : 'Reference copied'); 138 closeAllCiteMenus(); 139 }); 140 }); 141 142 return el; 143 } 144 145 function closeAllCiteMenus() { 146 document.querySelectorAll('.pub-cite.open').forEach(function (w) { 147 w.classList.remove('open'); 148 var b = w.querySelector('.cite-btn'); if (b) b.setAttribute('aria-expanded', 'false'); 149 }); 150 } 151 document.addEventListener('click', closeAllCiteMenus); 152 document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeAllCiteMenus(); }); 153 154 /* ---- Clipboard + toast ----------------------------------- */ 155 var toastEl; 156 function toast(msg) { 157 if (!toastEl) { 158 toastEl = document.createElement('div'); 159 toastEl.className = 'toast'; 160 toastEl.setAttribute('role', 'status'); 161 document.body.appendChild(toastEl); 162 } 163 toastEl.textContent = msg; 164 toastEl.classList.add('show'); 165 clearTimeout(toastEl._t); 166 toastEl._t = setTimeout(function () { toastEl.classList.remove('show'); }, 2200); 167 } 168 function copyText(text, okMsg) { 169 if (navigator.clipboard && navigator.clipboard.writeText) { 170 navigator.clipboard.writeText(text).then(function () { toast(okMsg); }, function () { fallbackCopy(text, okMsg); }); 171 } else { fallbackCopy(text, okMsg); } 172 } 173 function fallbackCopy(text, okMsg) { 174 var ta = document.createElement('textarea'); 175 ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; 176 document.body.appendChild(ta); ta.select(); 177 try { document.execCommand('copy'); toast(okMsg); } catch (e) { toast('Copy failed â select manually'); } 178 document.body.removeChild(ta); 179 } 180
181 /* ---- State + filtering ----------------------------------- */ 182 var state = { theme: 'all', type: 'all', q: '', sort: 'desc' }; 183 var listEl = document.getElementById('pub-list'); 184 var countEl = document.getElementById('pub-count'); 185 var emptyEl = document.getElementById('pub-empty'); 186 var nodes = []; 187 188 function render() { 189 var frag = document.createDocumentFragment(); 190 var sorted = PUBS.slice().sort(function (a, b) { return state.sort === 'asc' ? a.year - b.year : b.year - a.year; }); 191 nodes = sorted.map(function (p) { var n = buildNode(p); frag.appendChild(n); return { p: p, el: n }; }); 192 listEl.innerHTML = ''; 193 listEl.appendChild(frag); 194 applyFilters(); 195 } 196 197 function applyFilters() { 198 var shown = 0; 199 nodes.forEach(function (n) { 200 var p = n.p; 201 var okTheme = state.theme === 'all' || (p.themes || []).indexOf(state.theme) !== -1; 202 var okType = state.type === 'all' || p.type === state.type; 203 var okQ = !state.q || n.el.getAttribute('data-text').indexOf(state.q) !== -1; 204 var show = okTheme && okType && okQ; 205 if (show) { n.el.removeAttribute('hidden'); shown++; } else { n.el.setAttribute('hidden', ''); } 206 }); 207 countEl.textContent = shown + (shown === 1 ? ' publication' : ' publications'); 208 emptyEl.hidden = shown !== 0; 209 } 210 211 /* ---- Wire up controls ------------------------------------ */ 212 function initChips(groupId, key) { 213 var group = document.getElementById(groupId); 214 if (!group) return; 215 group.addEventListener('click', function (e) { 216 var chip = e.target.closest('.chip'); 217 if (!chip) return; 218 group.querySelectorAll('.chip').forEach(function (c) { c.classList.remove('active'); c.setAttribute('aria-pressed', 'false'); }); 219 chip.classList.add('active'); chip.setAttribute('aria-pressed', 'true'); 220 state[key] = chip.getAttribute('data-value'); 221 applyFilters(); 222 }); 223 } 224 225 document.addEventListener('DOMContentLoaded', function () { 226 initChips('filter-theme', 'theme'); 227 initChips('filter-type', 'type'); 228 229 var search = document.getElementById('pub-search'); 230 if (search) search.addEventListener('input', function () { state.q = this.value.trim().toLowerCase(); applyFilters(); }); 231 232 var sortBtn = document.getElementById('pub-sort'); 233 if (sortBtn) sortBtn.addEventListener('click', function () { 234 state.sort = state.sort === 'desc' ? 'asc' : 'desc'; 235 this.querySelector('.sort-val').textContent = state.sort === 'desc' ? 'Newest' : 'Oldest'; 236 render(); 237 }); 238 239 render(); 240 }); 241})();
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.