574 lines
27 KiB
JavaScript
574 lines
27 KiB
JavaScript
/* popups.js — Hover preview popups.
|
|
Content providers (in dispatch priority order):
|
|
1. Local annotations — /data/annotations.json (any URL, author-defined)
|
|
2. Citations — DOM lookup, cite-link[href^="#ref-"]
|
|
3. Internal pages — same-origin fetch, title + authors + tags + abstract + stats
|
|
4. Wikipedia — MediaWiki action API, full lead section
|
|
5. arXiv — export.arxiv.org Atom API
|
|
6. DOI / CrossRef — api.crossref.org, title/authors/abstract
|
|
7. GitHub — api.github.com, repo description + stars
|
|
8. Open Library — openlibrary.org JSON API, book description
|
|
9. bioRxiv / medRxiv — api.biorxiv.org, abstract
|
|
10. YouTube — oEmbed, title + channel (no key required)
|
|
11. Internet Archive — archive.org/metadata, title + description
|
|
12. PubMed — NCBI esummary, title + authors + journal
|
|
|
|
Production nginx CSP must add:
|
|
connect-src https://en.wikipedia.org https://export.arxiv.org
|
|
https://api.crossref.org https://api.github.com
|
|
https://openlibrary.org https://api.biorxiv.org
|
|
https://www.youtube.com https://archive.org
|
|
https://eutils.ncbi.nlm.nih.gov
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
var SHOW_DELAY = 250;
|
|
var HIDE_DELAY = 150;
|
|
|
|
var popup = null;
|
|
var showTimer = null;
|
|
var hideTimer = null;
|
|
var activeTarget = null;
|
|
var cache = Object.create(null); /* url → html; only successful results stored */
|
|
var annotations = null; /* null = not yet loaded */
|
|
|
|
/* ------------------------------------------------------------------
|
|
Init — load annotations first, then bind all targets
|
|
------------------------------------------------------------------ */
|
|
|
|
function init() {
|
|
popup = document.createElement('div');
|
|
popup.className = 'link-popup';
|
|
popup.setAttribute('aria-live', 'polite');
|
|
popup.setAttribute('aria-hidden', 'true');
|
|
document.body.appendChild(popup);
|
|
|
|
popup.addEventListener('mouseenter', cancelHide);
|
|
popup.addEventListener('mouseleave', scheduleHide);
|
|
|
|
loadAnnotations().then(function () {
|
|
bindTargets(document.body);
|
|
});
|
|
}
|
|
|
|
function bindTargets(root) {
|
|
/* Citation markers */
|
|
root.querySelectorAll('a.cite-link[href^="#ref-"]').forEach(function (el) {
|
|
bind(el, citationContent);
|
|
});
|
|
|
|
/* Internal links — absolute (/foo) and relative (../../foo) same-origin hrefs.
|
|
relativizeUrls in Hakyll makes index-page links relative, so we must match both. */
|
|
root.querySelectorAll('a[href^="/"], a[href^="./"], a[href^="../"]').forEach(function (el) {
|
|
/* Author links in .meta-authors and backlink source links always get popups */
|
|
var inAuthors = el.closest('.meta-authors');
|
|
var isBacklink = el.classList.contains('backlink-source');
|
|
if (!inAuthors && !isBacklink) {
|
|
if (el.closest('nav, #toc, footer, .page-meta-footer, .metadata')) return;
|
|
if (el.classList.contains('cite-link') || el.classList.contains('meta-tag')) return;
|
|
}
|
|
bind(el, internalContent);
|
|
});
|
|
|
|
/* PGP signature links in footer */
|
|
root.querySelectorAll('a.footer-sig-link').forEach(function (el) {
|
|
bind(el, sigContent);
|
|
});
|
|
|
|
/* External links — single dispatcher handles all providers */
|
|
root.querySelectorAll('a[href^="http"]').forEach(function (el) {
|
|
if (el.closest('nav, #toc, footer, .page-meta-footer')) return;
|
|
var provider = getProvider(el.getAttribute('href') || '');
|
|
if (provider) bind(el, provider);
|
|
});
|
|
}
|
|
|
|
/* Returns the appropriate provider function for a given URL, or null. */
|
|
function getProvider(href) {
|
|
if (!href) return null;
|
|
/* Local annotation takes priority over everything */
|
|
if (annotations && annotations[href]) return annotationContent;
|
|
if (/wikipedia\.org\/wiki\//.test(href)) return wikipediaContent;
|
|
if (/arxiv\.org\/(?:abs|pdf)\/\d{4}\.\d{4,5}/.test(href)) return arxivContent;
|
|
if (/(?:dx\.)?doi\.org\/10\./.test(href)) return doiContent;
|
|
if (/github\.com\/[^/]+\/[^/?#]+/.test(href)) return githubContent;
|
|
if (/openlibrary\.org\/(?:works|books)\//.test(href)) return openlibraryContent;
|
|
if (/(?:bio|med)rxiv\.org\/content\/10\./.test(href)) return biorxivContent;
|
|
if (/(?:youtube\.com\/watch|youtu\.be\/)/.test(href)) return youtubeContent;
|
|
if (/archive\.org\/details\//.test(href)) return archiveContent;
|
|
if (/pubmed\.ncbi\.nlm\.nih\.gov\/\d/.test(href)) return pubmedContent;
|
|
return null;
|
|
}
|
|
|
|
function bind(el, provider) {
|
|
el.addEventListener('mouseenter', function () { scheduleShow(el, provider); });
|
|
el.addEventListener('mouseleave', scheduleHide);
|
|
el.addEventListener('focus', function () { scheduleShow(el, provider); });
|
|
el.addEventListener('blur', scheduleHide);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------
|
|
Lifecycle
|
|
------------------------------------------------------------------ */
|
|
|
|
function scheduleShow(target, provider) {
|
|
cancelHide();
|
|
clearTimeout(showTimer);
|
|
activeTarget = target;
|
|
showTimer = setTimeout(function () {
|
|
provider(target).then(function (html) {
|
|
if (!html || activeTarget !== target) return;
|
|
popup.innerHTML = html;
|
|
positionPopup(target);
|
|
popup.classList.add('is-visible');
|
|
popup.setAttribute('aria-hidden', 'false');
|
|
}).catch(function () { /* silently fail */ });
|
|
}, SHOW_DELAY);
|
|
}
|
|
|
|
function scheduleHide() {
|
|
clearTimeout(showTimer);
|
|
hideTimer = setTimeout(function () {
|
|
popup.classList.remove('is-visible');
|
|
popup.setAttribute('aria-hidden', 'true');
|
|
activeTarget = null;
|
|
}, HIDE_DELAY);
|
|
}
|
|
|
|
function cancelHide() { clearTimeout(hideTimer); }
|
|
|
|
/* ------------------------------------------------------------------
|
|
Positioning — centres below target, flips above if clipped
|
|
------------------------------------------------------------------ */
|
|
|
|
function positionPopup(target) {
|
|
var rect = target.getBoundingClientRect();
|
|
var pw = popup.offsetWidth;
|
|
var ph = popup.offsetHeight;
|
|
var vw = window.innerWidth;
|
|
var vh = window.innerHeight;
|
|
var sy = window.scrollY;
|
|
var sx = window.scrollX;
|
|
var GAP = 10;
|
|
|
|
var left = rect.left + sx + rect.width / 2 - pw / 2;
|
|
left = Math.max(sx + GAP, Math.min(left, sx + vw - pw - GAP));
|
|
|
|
var top = (rect.bottom + GAP + ph <= vh)
|
|
? rect.bottom + sy + GAP
|
|
: rect.top + sy - ph - GAP;
|
|
|
|
popup.style.left = left + 'px';
|
|
popup.style.top = top + 'px';
|
|
}
|
|
|
|
/* ------------------------------------------------------------------
|
|
Content providers
|
|
------------------------------------------------------------------ */
|
|
|
|
/* 0. Local annotations — synchronous map lookup after eager load */
|
|
function loadAnnotations() {
|
|
if (annotations !== null) return Promise.resolve(annotations);
|
|
return fetch('/data/annotations.json', { credentials: 'same-origin' })
|
|
.then(function (r) { return r.ok ? r.json() : {}; })
|
|
.then(function (data) { annotations = data; return data; })
|
|
.catch(function () { annotations = {}; return {}; });
|
|
}
|
|
|
|
function annotationContent(target) {
|
|
var href = target.getAttribute('href');
|
|
var ann = href && annotations && annotations[href];
|
|
if (!ann) return Promise.resolve(null);
|
|
return Promise.resolve(
|
|
'<div class="popup-annotation">'
|
|
+ (ann.title ? '<div class="popup-title">' + esc(ann.title) + '</div>' : '')
|
|
+ (ann.annotation ? '<div class="popup-abstract">' + esc(ann.annotation) + '</div>' : '')
|
|
+ '</div>'
|
|
);
|
|
}
|
|
|
|
/* 1. Citations — synchronous DOM lookup */
|
|
function citationContent(target) {
|
|
return new Promise(function (resolve) {
|
|
var id = (target.getAttribute('href') || '').slice(1);
|
|
var entry = document.getElementById(id);
|
|
resolve(entry
|
|
? '<div class="popup-citation">' + entry.innerHTML + '</div>'
|
|
: null);
|
|
});
|
|
}
|
|
|
|
/* 2. Internal pages — same-origin fetch, rich preview */
|
|
function internalContent(target) {
|
|
/* Resolve relative hrefs (../../foo) to canonical path (/foo) for fetch + cache. */
|
|
var raw = target.getAttribute('href');
|
|
if (!raw) return Promise.resolve(null);
|
|
var href = new URL(raw, window.location.href).pathname;
|
|
if (cache[href]) return Promise.resolve(cache[href]);
|
|
|
|
return fetch(href, { credentials: 'same-origin' })
|
|
.then(function (r) { return r.ok ? r.text() : null; })
|
|
.then(function (text) {
|
|
if (!text) return null;
|
|
var doc = new DOMParser().parseFromString(text, 'text/html');
|
|
var titleEl = doc.querySelector('h1.page-title');
|
|
if (!titleEl) return null;
|
|
|
|
/* Abstract */
|
|
var abstrEl = doc.querySelector('.meta-description');
|
|
var abstract = abstrEl ? abstrEl.textContent.trim() : '';
|
|
if (abstract.length > 300)
|
|
abstract = abstract.slice(0, 300).replace(/\s\S+$/, '') + '\u2026';
|
|
|
|
/* Authors */
|
|
var authorEls = doc.querySelectorAll('.meta-authors a');
|
|
var authors = Array.from(authorEls).map(function (a) {
|
|
return a.textContent.trim();
|
|
}).join(', ');
|
|
|
|
/* Tags */
|
|
var tagEls = doc.querySelectorAll('.meta-tags a');
|
|
var tags = Array.from(tagEls).map(function (a) {
|
|
return a.textContent.trim();
|
|
}).join(' · ');
|
|
|
|
/* Reading stats — word count and reading time from meta block */
|
|
var wcEl = doc.querySelector('.meta-word-count');
|
|
var rtEl = doc.querySelector('.meta-reading-time');
|
|
var stats = [
|
|
wcEl ? wcEl.textContent.trim() : '',
|
|
rtEl ? rtEl.textContent.trim() : ''
|
|
].filter(Boolean).join(' · ');
|
|
|
|
return store(href,
|
|
'<div class="popup-internal">'
|
|
+ (tags ? '<div class="popup-source">' + esc(tags) + '</div>' : '')
|
|
+ '<div class="popup-title">' + esc(titleEl.textContent.trim()) + '</div>'
|
|
+ (authors ? '<div class="popup-authors">' + esc(authors) + '</div>' : '')
|
|
+ (abstract ? '<div class="popup-abstract">' + esc(abstract) + '</div>' : '')
|
|
+ (stats ? '<div class="popup-meta">' + esc(stats) + '</div>' : '')
|
|
+ '</div>');
|
|
})
|
|
.catch(function () { return null; });
|
|
}
|
|
|
|
/* 3. Wikipedia — MediaWiki action API, full lead section, text-only */
|
|
function wikipediaContent(target) {
|
|
var href = target.getAttribute('href');
|
|
if (!href || cache[href]) return Promise.resolve(cache[href] || null);
|
|
|
|
var m = href.match(/wikipedia\.org\/wiki\/([^#?]+)/);
|
|
if (!m) return Promise.resolve(null);
|
|
|
|
var apiUrl = 'https://en.wikipedia.org/w/api.php'
|
|
+ '?action=query&prop=extracts&exintro=1&format=json&redirects=1'
|
|
+ '&titles=' + encodeURIComponent(decodeURIComponent(m[1])) + '&origin=*';
|
|
|
|
return fetch(apiUrl)
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (data) {
|
|
var pages = data && data.query && data.query.pages;
|
|
if (!pages) return null;
|
|
var page = Object.values(pages)[0];
|
|
if (!page || page.missing !== undefined) return null;
|
|
var doc = new DOMParser().parseFromString(page.extract || '', 'text/html');
|
|
/* Remove math elements before extracting text — their DOM includes both
|
|
display characters and raw LaTeX source, producing garbled output. */
|
|
doc.querySelectorAll('.mwe-math-element').forEach(function (el) {
|
|
el.parentNode.removeChild(el);
|
|
});
|
|
var text = (doc.body.textContent || '').replace(/\s+/g, ' ').trim();
|
|
if (!text) return null;
|
|
if (text.length > 600) text = text.slice(0, 600).replace(/\s\S+$/, '') + '\u2026';
|
|
return store(href,
|
|
'<div class="popup-wikipedia">'
|
|
+ '<div class="popup-source">Wikipedia</div>'
|
|
+ '<div class="popup-title">' + esc(page.title) + '</div>'
|
|
+ '<div class="popup-extract">' + esc(text) + '</div>'
|
|
+ '</div>');
|
|
})
|
|
.catch(function () { return null; });
|
|
}
|
|
|
|
/* 4. arXiv — Atom API, title + authors + abstract */
|
|
function arxivContent(target) {
|
|
var href = target.getAttribute('href');
|
|
if (!href || cache[href]) return Promise.resolve(cache[href] || null);
|
|
|
|
var m = href.match(/arxiv\.org\/(?:abs|pdf)\/(\d{4}\.\d{4,5}(?:v\d+)?)/);
|
|
if (!m) return Promise.resolve(null);
|
|
|
|
var id = m[1].replace(/v\d+$/, '');
|
|
return fetch('https://export.arxiv.org/api/query?id_list=' + encodeURIComponent(id))
|
|
.then(function (r) { return r.ok ? r.text() : null; })
|
|
.then(function (xml) {
|
|
if (!xml) return null;
|
|
var doc = new DOMParser().parseFromString(xml, 'application/xml');
|
|
var titleEl = doc.querySelector('entry > title');
|
|
var summaryEl = doc.querySelector('entry > summary');
|
|
if (!titleEl || !summaryEl) return null;
|
|
var title = titleEl.textContent.trim().replace(/\s+/g, ' ');
|
|
var summary = summaryEl.textContent.trim().replace(/\s+/g, ' ');
|
|
if (summary.length > 500) summary = summary.slice(0, 500).replace(/\s\S+$/, '') + '\u2026';
|
|
var authors = Array.from(doc.querySelectorAll('entry > author > name'))
|
|
.map(function (el) { return el.textContent.trim(); });
|
|
var authorStr = authors.slice(0, 3).join(', ');
|
|
if (authors.length > 3) authorStr += ' et\u00a0al.';
|
|
return store(href,
|
|
'<div class="popup-arxiv">'
|
|
+ '<div class="popup-source">arXiv</div>'
|
|
+ '<div class="popup-title">' + esc(title) + '</div>'
|
|
+ (authorStr ? '<div class="popup-authors">' + esc(authorStr) + '</div>' : '')
|
|
+ '<div class="popup-abstract">' + esc(summary) + '</div>'
|
|
+ '</div>');
|
|
})
|
|
.catch(function () { return null; });
|
|
}
|
|
|
|
/* 5. DOI / CrossRef — title, authors, journal, year, abstract */
|
|
function doiContent(target) {
|
|
var href = target.getAttribute('href');
|
|
if (!href || cache[href]) return Promise.resolve(cache[href] || null);
|
|
|
|
var m = href.match(/(?:dx\.)?doi\.org\/(10\.[^?#\s]+)/);
|
|
if (!m) return Promise.resolve(null);
|
|
|
|
return fetch('https://api.crossref.org/works/' + encodeURIComponent(m[1]))
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (data) {
|
|
var msg = data && data.message;
|
|
if (!msg) return null;
|
|
var title = (msg.title && msg.title[0]) || '';
|
|
if (!title) return null;
|
|
var authors = (msg.author || []).slice(0, 3)
|
|
.map(function (a) { return (a.given ? a.given + ' ' : '') + (a.family || ''); })
|
|
.join(', ');
|
|
if ((msg.author || []).length > 3) authors += ' et\u00a0al.';
|
|
var journal = (msg['container-title'] && msg['container-title'][0]) || '';
|
|
var parts = msg.issued && msg.issued['date-parts'];
|
|
var year = parts && parts[0] && parts[0][0];
|
|
var abstract = (msg.abstract || '').replace(/<[^>]+>/g, '').trim();
|
|
if (abstract.length > 500) abstract = abstract.slice(0, 500).replace(/\s\S+$/, '') + '\u2026';
|
|
var meta = [journal, year].filter(Boolean).join(', ');
|
|
return store(href,
|
|
'<div class="popup-doi">'
|
|
+ '<div class="popup-source">CrossRef</div>'
|
|
+ '<div class="popup-title">' + esc(title) + '</div>'
|
|
+ (authors ? '<div class="popup-authors">' + esc(authors) + '</div>' : '')
|
|
+ (meta ? '<div class="popup-meta">' + esc(meta) + '</div>' : '')
|
|
+ (abstract ? '<div class="popup-abstract">' + esc(abstract) + '</div>' : '')
|
|
+ '</div>');
|
|
})
|
|
.catch(function () { return null; });
|
|
}
|
|
|
|
/* 6. GitHub — repo description, language, stars */
|
|
function githubContent(target) {
|
|
var href = target.getAttribute('href');
|
|
if (!href || cache[href]) return Promise.resolve(cache[href] || null);
|
|
|
|
var m = href.match(/github\.com\/([^/]+)\/([^/?#]+)/);
|
|
if (!m) return Promise.resolve(null);
|
|
|
|
return fetch('https://api.github.com/repos/' + m[1] + '/' + m[2],
|
|
{ headers: { 'Accept': 'application/vnd.github.v3+json' } })
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (data) {
|
|
if (!data || !data.full_name) return null;
|
|
var meta = [data.language, data.stargazers_count != null ? '\u2605\u00a0' + data.stargazers_count : null]
|
|
.filter(Boolean).join(' \u00b7 ');
|
|
return store(href,
|
|
'<div class="popup-github">'
|
|
+ '<div class="popup-source">GitHub</div>'
|
|
+ '<div class="popup-title">' + esc(data.full_name) + '</div>'
|
|
+ (data.description ? '<div class="popup-abstract">' + esc(data.description) + '</div>' : '')
|
|
+ (meta ? '<div class="popup-meta">' + esc(meta) + '</div>' : '')
|
|
+ '</div>');
|
|
})
|
|
.catch(function () { return null; });
|
|
}
|
|
|
|
/* 7. Open Library — book title + description */
|
|
function openlibraryContent(target) {
|
|
var href = target.getAttribute('href');
|
|
if (!href || cache[href]) return Promise.resolve(cache[href] || null);
|
|
|
|
var base = href.replace(/[?#].*$/, '');
|
|
var apiUrl = base + '.json';
|
|
|
|
return fetch(apiUrl)
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (data) {
|
|
if (!data || !data.title) return null;
|
|
var desc = data.description;
|
|
if (desc && typeof desc === 'object') desc = desc.value;
|
|
desc = (desc || '').replace(/\s+/g, ' ').trim();
|
|
if (desc.length > 300) desc = desc.slice(0, 300).replace(/\s\S+$/, '') + '\u2026';
|
|
return store(href,
|
|
'<div class="popup-openlibrary">'
|
|
+ '<div class="popup-source">Open Library</div>'
|
|
+ '<div class="popup-title">' + esc(data.title) + '</div>'
|
|
+ (desc ? '<div class="popup-abstract">' + esc(desc) + '</div>' : '')
|
|
+ '</div>');
|
|
})
|
|
.catch(function () { return null; });
|
|
}
|
|
|
|
/* 8. bioRxiv / medRxiv — abstract via biorxiv content server API */
|
|
function biorxivContent(target) {
|
|
var href = target.getAttribute('href');
|
|
if (!href || cache[href]) return Promise.resolve(cache[href] || null);
|
|
|
|
var m = href.match(/(?:bio|med)rxiv\.org\/content\/(10\.\d{4,}\/[^?#\s]+)/);
|
|
if (!m) return Promise.resolve(null);
|
|
|
|
var doi = m[1].replace(/v\d+$/, '');
|
|
var server = /medrxiv/.test(href) ? 'medrxiv' : 'biorxiv';
|
|
var label = server === 'medrxiv' ? 'medRxiv' : 'bioRxiv';
|
|
|
|
return fetch('https://api.biorxiv.org/details/' + server + '/' + encodeURIComponent(doi) + '/json')
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (data) {
|
|
var paper = data && data.collection && data.collection[0];
|
|
if (!paper || !paper.title) return null;
|
|
var abstract = (paper.abstract || '').replace(/\s+/g, ' ').trim();
|
|
if (abstract.length > 500) abstract = abstract.slice(0, 500).replace(/\s\S+$/, '') + '\u2026';
|
|
var authorStr = '';
|
|
if (paper.authors) {
|
|
var list = paper.authors.split(';').map(function (s) { return s.trim(); }).filter(Boolean);
|
|
authorStr = list.slice(0, 3).join(', ');
|
|
if (list.length > 3) authorStr += ' et\u00a0al.';
|
|
}
|
|
return store(href,
|
|
'<div class="popup-biorxiv">'
|
|
+ '<div class="popup-source">' + esc(label) + '</div>'
|
|
+ '<div class="popup-title">' + esc(paper.title) + '</div>'
|
|
+ (authorStr ? '<div class="popup-authors">' + esc(authorStr) + '</div>' : '')
|
|
+ (abstract ? '<div class="popup-abstract">' + esc(abstract) + '</div>' : '')
|
|
+ '</div>');
|
|
})
|
|
.catch(function () { return null; });
|
|
}
|
|
|
|
/* 9. YouTube — oEmbed, title + channel name */
|
|
function youtubeContent(target) {
|
|
var href = target.getAttribute('href');
|
|
if (!href || cache[href]) return Promise.resolve(cache[href] || null);
|
|
|
|
return fetch('https://www.youtube.com/oembed?url=' + encodeURIComponent(href) + '&format=json')
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (data) {
|
|
if (!data || !data.title) return null;
|
|
return store(href,
|
|
'<div class="popup-youtube">'
|
|
+ '<div class="popup-source">YouTube</div>'
|
|
+ '<div class="popup-title">' + esc(data.title) + '</div>'
|
|
+ (data.author_name ? '<div class="popup-authors">' + esc(data.author_name) + '</div>' : '')
|
|
+ '</div>');
|
|
})
|
|
.catch(function () { return null; });
|
|
}
|
|
|
|
/* 10. Internet Archive — title, creator, description */
|
|
function archiveContent(target) {
|
|
var href = target.getAttribute('href');
|
|
if (!href || cache[href]) return Promise.resolve(cache[href] || null);
|
|
|
|
var m = href.match(/archive\.org\/details\/([^/?#]+)/);
|
|
if (!m) return Promise.resolve(null);
|
|
|
|
return fetch('https://archive.org/metadata/' + encodeURIComponent(m[1]))
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (data) {
|
|
var meta = data && data.metadata;
|
|
if (!meta) return null;
|
|
var first = function (v) { return Array.isArray(v) ? v[0] : (v || ''); };
|
|
var title = first(meta.title);
|
|
var creator = first(meta.creator);
|
|
var year = first(meta.year);
|
|
var desc = first(meta.description);
|
|
if (!title) return null;
|
|
desc = desc.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
if (desc.length > 280) desc = desc.slice(0, 280).replace(/\s\S+$/, '') + '\u2026';
|
|
var byline = [creator, year].filter(Boolean).join(', ');
|
|
return store(href,
|
|
'<div class="popup-archive">'
|
|
+ '<div class="popup-source">Internet Archive</div>'
|
|
+ '<div class="popup-title">' + esc(title) + '</div>'
|
|
+ (byline ? '<div class="popup-authors">' + esc(byline) + '</div>' : '')
|
|
+ (desc ? '<div class="popup-abstract">' + esc(desc) + '</div>' : '')
|
|
+ '</div>');
|
|
})
|
|
.catch(function () { return null; });
|
|
}
|
|
|
|
/* 11. PubMed — NCBI esummary, title + authors + journal */
|
|
function pubmedContent(target) {
|
|
var href = target.getAttribute('href');
|
|
if (!href || cache[href]) return Promise.resolve(cache[href] || null);
|
|
|
|
var m = href.match(/pubmed\.ncbi\.nlm\.nih\.gov\/(\d+)/);
|
|
if (!m) return Promise.resolve(null);
|
|
|
|
var pmid = m[1];
|
|
var apiUrl = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi'
|
|
+ '?db=pubmed&id=' + pmid + '&retmode=json';
|
|
|
|
return fetch(apiUrl)
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (data) {
|
|
var paper = data && data.result && data.result[pmid];
|
|
if (!paper || !paper.title) return null;
|
|
var authors = (paper.authors || []).slice(0, 3)
|
|
.map(function (a) { return a.name; }).join(', ');
|
|
if ((paper.authors || []).length > 3) authors += ' et\u00a0al.';
|
|
var journal = paper.fulljournalname || paper.source || '';
|
|
var year = (paper.pubdate || '').slice(0, 4);
|
|
var meta = [journal, year].filter(Boolean).join(', ');
|
|
return store(href,
|
|
'<div class="popup-pubmed">'
|
|
+ '<div class="popup-source">PubMed</div>'
|
|
+ '<div class="popup-title">' + esc(paper.title) + '</div>'
|
|
+ (authors ? '<div class="popup-authors">' + esc(authors) + '</div>' : '')
|
|
+ (meta ? '<div class="popup-meta">' + esc(meta) + '</div>' : '')
|
|
+ '</div>');
|
|
})
|
|
.catch(function () { return null; });
|
|
}
|
|
|
|
/* ------------------------------------------------------------------
|
|
Helpers
|
|
------------------------------------------------------------------ */
|
|
|
|
function store(href, html) {
|
|
cache[href] = html;
|
|
return html;
|
|
}
|
|
|
|
/* PGP signature — fetch the .sig file and display ASCII armor */
|
|
function sigContent(target) {
|
|
var href = target.getAttribute('href');
|
|
if (!href) return Promise.resolve(null);
|
|
if (cache[href]) return Promise.resolve(cache[href]);
|
|
return fetch(href, { credentials: 'same-origin' })
|
|
.then(function (r) { return r.ok ? r.text() : null; })
|
|
.then(function (text) {
|
|
if (!text) return null;
|
|
var html = '<div class="popup-sig"><pre>' + esc(text.trim()) + '</pre></div>';
|
|
cache[href] = html;
|
|
return html;
|
|
});
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
}());
|