PageSourceSearch

https://nicolemarwell.github.io/pubs.js

js nicolemarwell.github.io collected 2026-10-03 09:40:57 UTC 11,141 bytes, 241 lines download raw bytes

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
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.