Customizable database access icons appear beside DOI URLs—preloaded with Anna's Archive (for books), SciDB, and Sci-Hub.
// ==UserScript==
// @name ScholarKey
// @namespace https://github.com/KHROTU
// @version 1.0.0
// @description Customizable database access icons appear beside DOI URLs—preloaded with Anna's Archive (for books), SciDB, and Sci-Hub.
// @author ezraiiiiiiiiiiii, KHROTU
// @match *://*/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'scholarkey_settings';
const DEFAULT_SETTINGS = {
wikipedia: { enabled: true, lang: 'en' },
sources: [
{
id: 'annas-search', emoji: '\uD83D\uDCD6', name: "Anna's Archive",
url: 'https://annas-archive.gl/search?q={DOI}', enabled: true
},
{
id: 'scihub-scidb', emoji: '\uD83E\uDDEC', name: 'Sci-Hub + SciDB',
url: JSON.stringify(['https://sci-hub.ru/{DOI}', 'https://annas-archive.gl/scidb/{DOI}']),
type: 'multi', enabled: true
}
],
behaviour: { scanBareText: true }
};
function loadSettings() {
try {
const raw = GM_getValue(STORAGE_KEY, null);
if (raw) {
const stored = JSON.parse(raw);
return Object.assign({}, DEFAULT_SETTINGS, stored, {
wikipedia: Object.assign({}, DEFAULT_SETTINGS.wikipedia, stored.wikipedia || {}),
behaviour: Object.assign({}, DEFAULT_SETTINGS.behaviour, stored.behaviour || {}),
sources: Array.isArray(stored.sources) && stored.sources.length
? stored.sources : DEFAULT_SETTINGS.sources
});
}
} catch (_) {}
return DEFAULT_SETTINGS;
}
function saveSettings() {
GM_setValue(STORAGE_KEY, JSON.stringify(settings));
}
let settings = loadSettings();
GM_registerMenuCommand(
'Wikipedia badge: ' + (settings.wikipedia.enabled ? 'ON' : 'OFF'),
function () {
settings.wikipedia.enabled = !settings.wikipedia.enabled;
saveSettings();
rerender();
}
);
GM_registerMenuCommand(
'Scan bare-text DOIs: ' + (settings.behaviour.scanBareText ? 'ON' : 'OFF'),
function () {
settings.behaviour.scanBareText = !settings.behaviour.scanBareText;
saveSettings();
rerender();
}
);
settings.sources.forEach(function (src, idx) {
GM_registerMenuCommand(
src.name + ': ' + (src.enabled ? 'ON' : 'OFF'),
function () {
settings.sources[idx].enabled = !settings.sources[idx].enabled;
saveSettings();
rerender();
}
);
});
GM_registerMenuCommand('Refresh Anna\'s & Sci-Hub URLs from Wikipedia', refreshUrlsFromWikipedia);
const WIKI_ICON = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHlsZT0ic2hhcGUtcmVuZGVyaW5nOmdlb21ldHJpY1ByZWNpc2lvbjsgZmlsbC1ydWxlOmV2ZW5vZGQiPg0KPHBhdGggZD0iTSAxMjAuODUsMjkuMjEgQyAxMjAuODUsMjkuNjIgMTIwLjcyLDI5Ljk5IDEyMC40NywzMC4zMyBDIDEyMC4yMSwzMC42NiAxMTkuOTQsMzAuODMgMTE5LjYzLDMwLjgzIEMgMTE3LjE0LDMxLjA3IDExNS4wOSwzMS44NyAxMTMuNTEsMzMuMjQgQyAxMTEuOTIsMzQuNiAxMTAuMjksMzcuMjEgMTA4LjYsNDEuMDUgTCA4Mi44LDk5LjE5IEMgODIuNjMsOTkuNzMgODIuMTYsMTAwIDgxLjM4LDEwMCBDIDgwLjc3LDEwMCA4MC4zLDk5LjczIDc5Ljk2LDk5LjE5IEwgNjUuNDksNjguOTMgTCA0OC44NSw5OS4xOSBDIDQ4LjUxLDk5LjczIDQ4LjA0LDEwMCA0Ny40MywxMDAgQyA0Ni42OSwxMDAgNDYuMiw5OS43MyA0NS45Niw5OS4xOSBMIDIwLjYxLDQxLjA1IEMgMTkuMDMsMzcuNDQgMTcuMzYsMzQuOTIgMTUuNiwzMy40OSBDIDEzLjg1LDMyLjA2IDExLjQsMzEuMTcgOC4yNywzMC44MyBDIDgsMzAuODMgNy43NCwzMC42OSA3LjUxLDMwLjQgQyA3LjI3LDMwLjEyIDcuMTUsMjkuNzkgNy4xNSwyOS40MiBDIDcuMTUsMjguNDcgNy40MiwyOCA3Ljk2LDI4IEMgMTAuMjIsMjggMTIuNTgsMjguMSAxNS4wNSwyOC4zIEMgMTcuMzQsMjguNTEgMTkuNSwyOC42MSAyMS41MiwyOC42MSBDIDIzLjU4LDI4LjYxIDI2LjAxLDI4LjUxIDI4LjgxLDI4LjMgQyAzMS43NCwyOC4xIDM0LjM0LDI4IDM2LjYsMjggQyAzNy4xNCwyOCAzNy40MSwyOC40NyAzNy40MSwyOS40MiBDIDM3LjQxLDMwLjM2IDM3LjI0LDMwLjgzIDM2LjkxLDMwLjgzIEMgMzQuNjUsMzEgMzIuODcsMzEuNTggMzEuNTcsMzIuNTUgQyAzMC4yNywzMy41MyAyOS42MiwzNC44MSAyOS42MiwzNi40IEMgMjkuNjIsMzcuMjEgMjkuODksMzguMjIgMzAuNDMsMzkuNDMgTCA1MS4zOCw4Ni43NCBMIDYzLjI3LDY0LjI4IEwgNTIuMTksNDEuMDUgQyA1MC4yLDM2LjkxIDQ4LjU2LDM0LjIzIDQ3LjI4LDMzLjAzIEMgNDYsMzEuODQgNDQuMDYsMzEuMSA0MS40NiwzMC44MyBDIDQxLjIyLDMwLjgzIDQxLDMwLjY5IDQwLjc4LDMwLjQgQyA0MC41NiwzMC4xMiA0MC40NSwyOS43OSA0MC40NSwyOS40MiBDIDQwLjQ1LDI4LjQ3IDQwLjY4LDI4IDQxLjE2LDI4IEMgNDMuNDIsMjggNDUuNDksMjguMSA0Ny4zOCwyOC4zIEMgNDkuMiwyOC41MSA1MS4xNCwyOC42MSA1My4yLDI4LjYxIEMgNTUuMjIsMjguNjEgNTcuMzYsMjguNTEgNTkuNjIsMjguMyBDIDYxLjk1LDI4LjEgNjQuMjQsMjggNjYuNSwyOCBDIDY3LjA0LDI4IDY3LjMxLDI4LjQ3IDY3LjMxLDI5LjQyIEMgNjcuMzEsMzAuMzYgNjcuMTQsMzAuODMgNjYuODEsMzAuODMgQyA2NC41NSwzMSA2Mi43NywzMS41OCA2MS40NywzMi41NSBDIDYwLjE3LDMzLjUzIDU5LjUyLDM0LjgxIDU5LjUyLDM2LjQgQyA1OS41MiwzNy4yMSA1OS43OSwzOC4yMiA2MC4zMywzOS40MyBMIDgxLjI4LDg2Ljc0IEwgOTMuMTcsNjQuMjggTCA4Mi4wOSw0MS4wNSBDIDgwLjEsMzYuOTEgNzguNDYsMzQuMjMgNzcuMTgsMzMuMDMgQyA3NS45LDMxLjg0IDczLjk2LDMxLjEgNzEuMzYsMzAuODMgQyA3MS4xMiwzMC44MyA3MC45LDMwLjY5IDcwLjY4LDMwLjQgQyA3MC40NiwzMC4xMiA3MC4zNSwyOS43OSA3MC4zNSwyOS40MiBDIDcwLjM1LDI4LjQ3IDcwLjU4LDI4IDcxLjA2LDI4IEMgNzMuMzIsMjggNzUuMzksMjguMSA3Ny4yOCwyOC4zIEMgNzkuMSwyOC41MSA4MS4wNCwyOC42MSA4My4xLDI4LjYxIEMgODUuMTIsMjguNjEgODcuMjYsMjguNTEgODkuNTIsMjguMyBDIDkxLjg1LDI4LjEgOTQuMTQsMjggOTYuNCwyOCBDIDk2Ljk0LDI4IDk3LjIxLDI4LjQ3IDk3LjIxLDI5LjQyIEMgOTcuMjEsMzAuMzYgOTcuMDQsMzAuODMgOTYuNzEsMzAuODMgQyA5NC40NSwzMSA5Mi42NywzMS41OCA5MS4zNywzMi41NSBDIDkwLjA3LDMzLjUzIDg5LjQyLDM0LjgxIDg5LjQyLDM2LjQgQyA4OS40MiwzNy4yMSA4OS42OSwzOC4yMiA5MC4yMywzOS40MyBMIDExMS4xOCw4Ni43NCBMIDEyMy4wNyw2NC4yOCBMIDExMS45OSw0MS4wNSBDIDExMCwzNi45MSAxMDguMzYsMzQuMjMgMTA3LjA4LDMzLjAzIEMgMTA1LjgsMzEuODQgMTAzLjg2LDMxLjEgMTAxLjI2LDMwLjgzIEMgMTAxLjAyLDMwLjgzIDEwMC44LDMwLjY5IDEwMC41OCwzMC40IEMgMTAwLjM2LDMwLjEyIDEwMC4yNSwyOS43OSAxMDAuMjUsMjkuNDIgQyAxMDAuMjUsMjguNDcgMTAwLjQ4LDI4IDEwMC45NiwyOCBDIDEwMy4yMiwyOCAxMDUuMjksMjguMSAxMDcuMTgsMjguMyBDIDEwOSwyOC41MSAxMTAuOTQsMjguNjEgMTEzLDI4LjYxIEMgMTE1LjAyLDI4LjYxIDExNy4xNiwyOC41MSAxMTkuNDIsMjguMyBDIDEyMS43NSwyOC4xIDEyNC4wNCwyOCAxMjYuMywyOCBDIDEyNi44NCwyOCAxMjcuMTEsMjguNDcgMTI3LjExLDI5LjQyIEMgMTI3LjExLDI5Ljc5IDEyNi45OSwzMC4xMiAxMjYuNzUsMzAuNCBDIDEyNi41MiwzMC42OSAxMjYuMjYsMzAuODMgMTI1Ljk5LDMwLjgzIEMgMTIzLjUsMzEuMDcgMTIxLjQ1LDMxLjg3IDExOS44NywzMy4yNCBDIDExOC4yOCwzNC42IDExNi42NSwzNy4yMSAxMTQuOTYsNDEuMDUgTCA4OS4xNiw5OS4xOSBDIDg4Ljk5LDk5LjczIDg4LjUyLDEwMCA4Ny43NCwxMDAgQyA4Ny4xMywxMDAgODYuNjYsOTkuNzMgODYuMzIsOTkuMTkgTCA3MS44NSw2OC45MyBMIDU1LjIxLDk5LjE5IEMgNTQuODcsOTkuNzMgNTQuNCwxMDAgNTMuNzksMTAwIEMgNTMuMDUsMTAwIDUyLjU2LDk5LjczIDUyLjMyLDk5LjE5IEwgMjYuOTcsNDEuMDUgQyAyNS4zOSwzNy40NCAyMy43MiwzNC45MiAyMS45NiwzMy40OSBDIDIwLjIxLDMyLjA2IDE3Ljc2LDMxLjE3IDE0LjYzLDMwLjgzIEMgMTQuMzYsMzAuODMgMTQuMSwzMC42OSAxMy44NywzMC40IEMgMTMuNjMsMzAuMTIgMTMuNTEsMjkuNzkgMTMuNTEsMjkuNDIgQyAxMy41MSwyOC40NyAxMy43OCwyOCAxNC4zMiwyOCBDIDE2LjU4LDI4IDE4Ljk0LDI4LjEgMjEuNDEsMjguMyBDIDIzLjcsMjguNTEgMjUuODYsMjguNjEgMjcuODgsMjguNjEgQyAyOS45NCwyOC42MSAzMi4zNywyOC41MSAzNS4xNywyOC4zIEMgMzguMSwyOC4xIDQwLjcsMjggNDIuOTYsMjggQyA0My41LDI4IDQzLjc3LDI4LjQ3IDQzLjc3LDI5LjQyIEMgNDMuNzcsMzAuMzYgNDMuNiwzMC44MyA0My4yNywzMC44MyBDIDQxLjAxLDMxIDM5LjIzLDMxLjU4IDM3LjkzLDMyLjU1IEMgMzYuNjMsMzMuNTMgMzUuOTgsMzQuODEgMzUuOTgsMzYuNCBDIDM1Ljk4LDM3LjIxIDM2LjI1LDM4LjIyIDM2Ljc5LDM5LjQzIEwgNTcuNzQsODYuNzQgTCA2OS42Myw2NC4yOCBMIDU4LjU1LDQxLjA1IEMgNTYuNTYsMzYuOTEgNTQuOTIsMzQuMjMgNTMuNjQsMzMuMDMgQyA1Mi4zNiwzMS44NCA1MC40MiwzMS4xIDQ3LjgyLDMwLjgzIEMgNDcuNTgsMzAuODMgNDcuMzYsMzAuNjkgNDcuMTQsMzAuNCBDIDQ2LjkyLDMwLjEyIDQ2LjgxLDI5Ljc5IDQ2LjgxLDI5LjQyIEMgNDYuODEsMjguNDcgNDcuMDQsMjggNDcuNTIsMjggQyA0OS43OCwyOCA1MS44NSwyOC4xIDUzLjc0LDI4LjMgQyA1NS41NiwyOC41MSA1Ny41LDI4LjYxIDU5LjU2LDI4LjYxIEMgNjEuNTgsMjguNjEgNjMuNzIsMjguNTEgNjUuOTgsMjguMyBDIDY4LjMxLDI4LjEgNzAuNiwyOCA3Mi44NiwyOCBDIDczLjQsMjggNzMuNjcsMjguNDcgNzMuNjcsMjkuNDIiLz4NCjwvc3ZnPg==';
const DOI_HREF_RE = /^https?:\/\/(?:dx\.)?doi\.org\/(.+)/i;
const DOI_AFTER_SEGMENT_RE = /\/doi\/(10\.\d{4,}\/[^\s<>"'?#&]+)/i;
const DOI_TEXT_TEST = /\b10\.\d{4,}\/[^\s<>"']+/;
const doiTextRe = function () {
return /\b(10\.\d{4,}\/[^\s<>"']+)/g;
};
const wikiCache = new Map();
let processed = new WeakSet();
const decoratedDois = new Set();
let activePopup = null;
let hideTimer = null;
const ON_WIKIPEDIA = /\.wikipedia\.org$/.test(location.hostname);
const WIKI_CONCURRENCY = 5;
let wikiInFlight = 0;
const wikiQueue = [];
function wikiEnqueue(fn) {
return new Promise(function (resolve, reject) {
wikiQueue.push(function () { return fn().then(resolve, reject); });
wikiDrain();
});
}
function wikiDrain() {
while (wikiInFlight < WIKI_CONCURRENCY && wikiQueue.length) {
wikiInFlight++;
wikiQueue.shift()().finally(function () { wikiInFlight--; wikiDrain(); });
}
}
const wikiLang = function () { return settings.wikipedia.lang || 'en'; };
const showBadge = function () { return settings.wikipedia.enabled !== false; };
function doiQueryString(doi) {
return 'insource:"' + doi.replace(/#/g, '%23').replace(/&/g, '%26') + '"';
}
function wikiSearchUrl(doi) {
return 'https://' + wikiLang() + '.wikipedia.org/w/index.php' +
'?search=' + doiQueryString(doi) +
'&title=Special%3ASearch&profile=advanced&fulltext=1&ns0=1';
}
function wikiSearchApiUrl(doi) {
return 'https://' + wikiLang() + '.wikipedia.org/w/api.php' +
'?action=query&list=search&srnamespace=0&srlimit=5&utf8=&format=json&origin=*' +
'&srsearch=' + doiQueryString(doi);
}
function wikiPageDetailsUrl(pageids) {
return 'https://' + wikiLang() + '.wikipedia.org/w/api.php' +
'?action=query&pageids=' + pageids.join('|') +
'&prop=extracts|pageimages&exintro=1&explaintext=1&exchars=280' +
'&piprop=thumbnail&pithumbsize=80&format=json&origin=*';
}
function applyTemplate(tpl, doi) {
var doiUrl = 'https://doi.org/' + encodeURIComponent(doi);
return tpl
.replace(/\{DOI_URL\}/g, doiUrl)
.replace(/\{DOI\}/g, doi)
.replace(/EXAMPLE_DOI/g, doi);
}
const normaliseDoi = function (raw) { return raw.replace(/[.,;)\]}"']+$/, ''); };
async function fetchFirstUrlFromWikitext(title) {
const apiUrl = 'https://en.wikipedia.org/w/api.php' +
'?action=query&titles=' + encodeURIComponent(title) +
'&prop=revisions&rvprop=content&rvslots=main&format=json&origin=*';
const res = await fetch(apiUrl);
const json = await res.json();
const pages = json && json.query && json.query.pages || {};
const wikitext = (Object.values(pages)[0] && Object.values(pages)[0].revisions && Object.values(pages)[0].revisions[0] && Object.values(pages)[0].revisions[0].slots && Object.values(pages)[0].revisions[0].slots.main && Object.values(pages)[0].revisions[0].slots.main['*']) || '';
const urlBlock = wikitext.match(/\|\s*url\s*=\s*([\s\S]*?)(?=\n\s*\||}})/i);
if (!urlBlock) return null;
const urlMatch = urlBlock[1].match(/https?:\/\/[^\s\]\[}{|<>"]+/);
return urlMatch ? urlMatch[0].replace(/\/$/, '') : null;
}
async function refreshUrlsFromWikipedia() {
try {
var annasUrl = await fetchFirstUrlFromWikitext("Anna's Archive");
var scihubUrl = await fetchFirstUrlFromWikitext('Sci-Hub');
const replaceHost = function (u) {
if (annasUrl && /annas-archive/i.test(u)) {
return u.replace(/^https?:\/\/[^\/]+/, annasUrl);
}
if (scihubUrl && /sci-hub/i.test(u)) {
return u.replace(/^https?:\/\/[^\/]+/, scihubUrl);
}
return u;
};
var touched = 0;
for (var i = 0; i < settings.sources.length; i++) {
var src = settings.sources[i];
if (src.type === 'multi') {
var arr = [];
try { arr = JSON.parse(src.url || '[]'); } catch (_) {}
if (Array.isArray(arr) && arr.length) {
var rewritten = arr.map(replaceHost);
settings.sources[i].url = JSON.stringify(rewritten);
if (rewritten.some(function (r, j) { return r !== arr[j]; })) touched++;
}
} else if (src.url) {
var prev = src.url;
settings.sources[i].url = replaceHost(src.url);
if (settings.sources[i].url !== prev) touched++;
}
}
saveSettings();
rerender();
var msg = 'ScholarKey: ';
if (annasUrl) msg += "Anna's \u2192 " + annasUrl + ' ';
if (scihubUrl) msg += 'Sci-Hub \u2192 ' + scihubUrl + ' ';
if (touched) msg += '(' + touched + ' URL' + (touched !== 1 ? 's' : '') + ' updated)';
console.log(msg);
} catch (e) {
console.warn('ScholarKey: URL refresh failed:', e);
}
}
async function fetchWikiCitations(doi) {
if (ON_WIKIPEDIA) return null;
const key = wikiLang() + '::' + doi;
if (wikiCache.has(key)) return wikiCache.get(key);
return wikiEnqueue(async function () {
if (wikiCache.has(key)) return wikiCache.get(key);
try {
const res = await fetch(wikiSearchApiUrl(doi));
if (!res.ok) throw new Error('HTTP ' + res.status);
const json = await res.json();
const hits = (json && json.query && json.query.search) || [];
const count = (json && json.query && json.query.searchinfo && json.query.searchinfo.totalhits) || 0;
var pageDetails = {};
if (hits.length > 0) {
try {
const r2 = await fetch(wikiPageDetailsUrl(hits.map(function (h) { return h.pageid; })));
const j2 = await r2.json();
pageDetails = (j2 && j2.query && j2.query.pages) || {};
} catch (_) {}
}
const result = { count: count, hits: hits, pageDetails: pageDetails };
wikiCache.set(key, result);
return result;
} catch (e) {
console.warn('[ScholarKey]', doi, e);
return null;
}
});
}
function buildRow(doi) {
const row = document.createElement('span');
row.className = 'sk-row';
row.dataset.doi = doi;
if (showBadge()) {
const wiki = document.createElement('a');
wiki.className = 'sk-wiki';
wiki.href = wikiSearchUrl(doi);
wiki.target = '_blank';
wiki.rel = 'noopener noreferrer';
wiki.innerHTML =
'<img class="sk-wiki__icon" src="' + WIKI_ICON + '" alt="" width="12" height="12">' +
'<span class="sk-wiki__count">\u2026</span>';
if (ON_WIKIPEDIA) {
wiki.setAttribute('aria-label', 'Search Wikipedia for articles citing this DOI');
wiki.querySelector('.sk-wiki__count').textContent = '?';
} else {
wiki.classList.add('sk-wiki--loading');
wiki.setAttribute('aria-label', 'Loading Wikipedia citations\u2026');
fetchWikiCitations(doi).then(function (data) {
if (data) {
renderWikiBadge(wiki, doi, data);
} else {
wiki.classList.remove('sk-wiki--loading');
wiki.setAttribute('aria-label', 'Search Wikipedia for articles citing this DOI');
wiki.querySelector('.sk-wiki__count').textContent = '?';
}
});
}
row.appendChild(wiki);
}
settings.sources.forEach(function (src) {
if (!src.enabled) return;
row.appendChild(makeSourceBtn(src, doi));
});
return row;
}
function eduDomainFromUrl(url) {
var m = url && url.match(/([a-z0-9-]+\.edu)/i);
return m ? m[1].toLowerCase() : null;
}
function makeSourceBtn(src, doi) {
var btn = document.createElement('a');
btn.className = 'sk-src';
btn.title = src.name;
btn.setAttribute('aria-label', 'Open ' + doi + ' on ' + src.name);
var firstUrl = src.type === 'multi'
? (function () { try { return JSON.parse(src.url)[0]; } catch (_) { return src.url; } })()
: src.url;
var edu = eduDomainFromUrl(firstUrl);
if (edu) {
var img = document.createElement('img');
img.src = 'https://www.google.com/s2/favicons?domain=' + edu + '&sz=32';
img.alt = '';
img.width = 14;
img.height = 14;
img.className = 'sk-src__favicon';
btn.appendChild(img);
} else {
btn.textContent = src.emoji || '\uD83D\uDD17';
}
btn.target = '_blank';
btn.rel = 'noopener noreferrer';
if (src.type === 'multi') {
var urls;
try { urls = JSON.parse(src.url); } catch (_) { urls = [src.url]; }
var resolved = urls.map(function (u) { return applyTemplate(u, doi); });
btn.href = resolved[0];
btn.addEventListener('click', (function (rest) {
return function () {
rest.forEach(function (u) { window.open(u, '_blank', 'noopener,noreferrer'); });
};
})(resolved.slice(1)));
} else {
btn.href = applyTemplate(src.url, doi);
}
return btn;
}
function renderWikiBadge(badge, doi, data) {
badge.classList.remove('sk-wiki--loading');
badge.setAttribute('aria-label',
data.count + ' Wikipedia article' + (data.count !== 1 ? 's' : '') + ' cite this DOI');
if (data.count === 0) badge.classList.add('sk-wiki--zero');
badge.querySelector('.sk-wiki__count').textContent =
data.count > 999 ? '999+' : String(data.count);
badge.addEventListener('mouseenter', function () {
clearTimeout(hideTimer);
showWikiPopup(badge, doi, data);
});
badge.addEventListener('mouseleave', function () {
hideTimer = setTimeout(closePopup, 200);
});
}
function closePopup() {
if (activePopup) { activePopup.remove(); activePopup = null; }
}
function showWikiPopup(anchor, doi, data) {
closePopup();
const popup = document.createElement('div');
popup.className = 'sk-popup';
const header = document.createElement('div');
header.className = 'sk-popup__header';
const titleEl = document.createElement('span');
titleEl.className = 'sk-popup__title';
titleEl.textContent = wikiLang().toUpperCase() + ' Wikipedia citations';
const totalEl = document.createElement('span');
totalEl.className = 'sk-popup__total';
totalEl.textContent = data.count === 0
? 'None found'
: data.count + ' article' + (data.count !== 1 ? 's' : '');
header.append(titleEl, totalEl);
popup.appendChild(header);
const doiLine = document.createElement('div');
doiLine.className = 'sk-popup__doi';
doiLine.textContent = doi;
popup.appendChild(doiLine);
if (data.hits.length > 0) {
const list = document.createElement('ul');
list.className = 'sk-popup__list';
data.hits.forEach(function (hit) {
var pageData = data.pageDetails[String(hit.pageid)] || {};
var thumb = pageData.thumbnail;
var extract = (pageData.extract || '').trim();
const li = document.createElement('li');
li.className = 'sk-popup__item';
if (thumb && thumb.source) {
const img = document.createElement('img');
img.className = 'sk-popup__thumb';
img.src = thumb.source;
img.width = thumb.width || 56;
img.height = thumb.height || 56;
img.alt = '';
img.loading = 'lazy';
li.appendChild(img);
}
const text = document.createElement('div');
text.className = 'sk-popup__text';
const a = document.createElement('a');
a.href = 'https://' + wikiLang() + '.wikipedia.org/wiki/' +
encodeURIComponent(hit.title.replace(/ /g, '_'));
a.target = '_blank';
a.rel = 'noopener noreferrer';
a.className = 'sk-popup__article-title';
a.textContent = hit.title;
text.appendChild(a);
if (extract) {
var trimmed = extract.length > 220
? extract.slice(0, 220).replace(/\s\S+$/, '') + '\u2026'
: extract;
const snip = document.createElement('p');
snip.className = 'sk-popup__snippet';
snip.textContent = trimmed;
text.appendChild(snip);
}
li.appendChild(text);
list.appendChild(li);
});
popup.appendChild(list);
if (data.count > data.hits.length) {
const more = document.createElement('a');
more.className = 'sk-popup__more';
more.href = wikiSearchUrl(doi);
more.target = '_blank';
more.rel = 'noopener noreferrer';
more.textContent = 'View all ' + data.count + ' on Wikipedia \u2192';
popup.appendChild(more);
}
} else {
const empty = document.createElement('p');
empty.className = 'sk-popup__empty';
empty.textContent = 'No Wikipedia articles cite this DOI.';
popup.appendChild(empty);
}
popup.addEventListener('mouseenter', function () { clearTimeout(hideTimer); });
popup.addEventListener('mouseleave', function () { hideTimer = setTimeout(closePopup, 200); });
document.body.appendChild(popup);
activePopup = popup;
const rect = anchor.getBoundingClientRect();
const popW = 340;
const popH = popup.offsetHeight || 260;
var left = rect.left + window.scrollX;
var top = rect.bottom + window.scrollY + 6;
if (rect.bottom + 6 + popH > window.innerHeight) top = rect.top + window.scrollY - popH - 6;
if (left + popW > window.innerWidth + window.scrollX) left = window.innerWidth + window.scrollX - popW - 8;
if (left < window.scrollX + 4) left = window.scrollX + 4;
popup.style.cssText = 'left:' + left + 'px;top:' + top + 'px;width:' + popW + 'px';
}
const rowAnchorMap = new Map();
function injectRow(doi, refNode) {
if (processed.has(refNode)) return;
const canonical = normaliseDoi(doi);
if (decoratedDois.has(canonical)) return;
processed.add(refNode);
decoratedDois.add(canonical);
const row = buildRow(canonical);
if (refNode.parentNode) {
observer.disconnect();
refNode.parentNode.insertBefore(row, refNode.nextSibling);
rowAnchorMap.set(row, refNode);
observer.observe(document.body, OBSERVER_OPTS);
}
}
function extractDoiFromAnchor(a) {
const href = a.getAttribute('href') || '';
const m1 = href.match(DOI_HREF_RE);
if (m1) {
try { return decodeURIComponent(m1[1]); } catch (_) { return m1[1]; }
}
const dataDoi = a.getAttribute('data-doi') || a.getAttribute('data-doi-id') ||
a.getAttribute('data-article-doi');
if (dataDoi && /^10\.\d{4,}\//.test(dataDoi.trim())) return dataDoi.trim();
const m3 = href.match(DOI_AFTER_SEGMENT_RE);
if (m3) {
try { return decodeURIComponent(m3[1]); } catch (_) { return m3[1]; }
}
return null;
}
function scanAnchors(root) {
if (!root || !root.querySelectorAll) return;
root.querySelectorAll('a[href], a[data-doi], a[data-doi-id], a[data-article-doi]').forEach(function (a) {
if (processed.has(a) && a.isConnected) return;
if (a.closest && a.closest('.sk-row, .sk-popup')) return;
const doi = extractDoiFromAnchor(a);
if (doi) injectRow(doi, a);
});
}
function scanTextNodesOnce(root) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode: function (node) {
const tag = node.parentElement && node.parentElement.tagName
? node.parentElement.tagName.toUpperCase() : '';
if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT'].indexOf(tag) !== -1)
return NodeFilter.FILTER_REJECT;
if (node.parentElement && node.parentElement.closest &&
node.parentElement.closest('.sk-row,.sk-popup,.sk-src,.sk-wiki'))
return NodeFilter.FILTER_REJECT;
if (node.parentElement && node.parentElement.closest &&
node.parentElement.closest("a[href*='doi.org']"))
return NodeFilter.FILTER_REJECT;
return DOI_TEXT_TEST.test(node.nodeValue)
? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
});
const nodes = [];
var n;
while ((n = walker.nextNode())) nodes.push(n);
observer.disconnect();
const pending = [];
nodes.forEach(function (textNode) {
if (processed.has(textNode) || !textNode.parentNode) return;
processed.add(textNode);
const val = textNode.nodeValue;
const re = doiTextRe();
const frag = document.createDocumentFragment();
var match, lastIndex = 0;
while ((match = re.exec(val)) !== null) {
if (match.index > lastIndex)
frag.appendChild(document.createTextNode(val.slice(lastIndex, match.index)));
const span = document.createElement('span');
span.className = 'sk-inline';
span.textContent = match[0];
frag.appendChild(span);
pending.push([span, normaliseDoi(match[1])]);
lastIndex = re.lastIndex;
}
if (lastIndex > 0) {
if (lastIndex < val.length)
frag.appendChild(document.createTextNode(val.slice(lastIndex)));
textNode.parentNode.replaceChild(frag, textNode);
}
});
observer.observe(document.body, OBSERVER_OPTS);
pending.forEach(function (pair) {
requestAnimationFrame(function () { injectRow(pair[1], pair[0]); });
});
}
const OBSERVER_OPTS = { childList: true, subtree: true };
var mutationTimer = null;
const pendingRoots = new Set();
const observer = new MutationObserver(function (mutations) {
var hasRealMutation = false;
mutations.forEach(function (m) {
m.addedNodes.forEach(function (node) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.classList && (
node.classList.contains('sk-row') ||
node.classList.contains('sk-popup')
)) return;
pendingRoots.add(node);
hasRealMutation = true;
}
});
m.removedNodes.forEach(function (node) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.classList && node.classList.contains('sk-row')) return;
rowAnchorMap.forEach(function (anchor, row) {
if (node === anchor || (node.contains && node.contains(anchor))) {
var doi = row.dataset && row.dataset.doi;
if (doi) decoratedDois.delete(doi);
processed = new WeakSet();
rowAnchorMap.delete(row);
}
});
}
});
});
if (!hasRealMutation) return;
clearTimeout(mutationTimer);
mutationTimer = setTimeout(function () {
const roots = Array.from(pendingRoots);
pendingRoots.clear();
roots.forEach(function (root) { scanAnchors(root); });
}, 300);
});
function rerender() {
document.querySelectorAll('.sk-row').forEach(function (el) { el.remove(); });
decoratedDois.clear();
processed = new WeakSet();
closePopup();
document.querySelectorAll('span.sk-inline').forEach(function (span) {
const m = doiTextRe().exec(span.textContent);
if (m) {
const fresh = span.cloneNode(true);
span.replaceWith(fresh);
injectRow(normaliseDoi(m[1]), fresh);
}
});
scanAnchors(document.body);
}
function init() {
scanAnchors(document.body);
if (settings.behaviour && settings.behaviour.scanBareText) scanTextNodesOnce(document.body);
observer.observe(document.body, OBSERVER_OPTS);
[800, 2000, 4000].forEach(function (delay) {
setTimeout(function () {
scanAnchors(document.body);
if (settings.behaviour && settings.behaviour.scanBareText) scanTextNodesOnce(document.body);
}, delay);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
GM_addStyle([
'.sk-row {',
' display: inline-flex !important;',
' align-items: center !important;',
' gap: 3px !important;',
' margin-left: 4px !important;',
' vertical-align: middle !important;',
' white-space: nowrap !important;',
'}',
'.sk-wiki {',
' display: inline-flex !important;',
' align-items: center !important;',
' gap: 3px !important;',
' padding: 1px 5px 1px 3px !important;',
' border-radius: 10px !important;',
' background: #eaf3fb !important;',
' border: 1px solid #a2c4e0 !important;',
' color: #2563a8 !important;',
" font-family: 'Linux Libertine', Georgia, serif !important;",
' font-size: 0.72em !important;',
' font-weight: 600 !important;',
' line-height: 1.4 !important;',
' text-decoration: none !important;',
' cursor: pointer !important;',
' vertical-align: middle !important;',
' overflow: hidden !important;',
' box-sizing: border-box !important;',
' transition: background 0.15s, border-color 0.15s, box-shadow 0.15s !important;',
'}',
'.sk-wiki:hover {',
' background: #d0e8f8 !important;',
' border-color: #2563a8 !important;',
' box-shadow: 0 1px 4px rgba(37,99,168,0.18) !important;',
' text-decoration: none !important;',
'}',
'.sk-wiki--zero { background: #f5f5f5 !important; border-color: #ccc !important; color: #888 !important; }',
'.sk-wiki--loading { opacity: 0.55 !important; }',
'.sk-wiki__icon {',
' width: 12px !important;',
' height: 12px !important;',
' flex-shrink: 0 !important;',
' display: block !important;',
'}',
'.sk-wiki--zero .sk-wiki__icon { opacity: 0.4 !important; filter: grayscale(1) !important; }',
'.sk-wiki__count { font-variant-numeric: tabular-nums !important; }',
'.sk-src {',
' display: inline-flex !important;',
' align-items: center !important;',
' font-size: 14px !important;',
' line-height: 1 !important;',
' text-decoration: none !important;',
' opacity: 0.85 !important;',
' vertical-align: middle !important;',
' transition: opacity 0.12s, transform 0.1s !important;',
'}',
'.sk-src:hover {',
' opacity: 1 !important;',
' transform: translateY(-1px) !important;',
' text-decoration: none !important;',
'}',
'.sk-popup {',
' position: absolute;',
' z-index: 2147483647;',
' background: #fff;',
' border: 1px solid #a2c4e0;',
' border-radius: 6px;',
' box-shadow: 0 4px 20px rgba(0,0,0,0.14), 0 1px 4px rgba(0,0,0,0.08);',
" font-family: 'Linux Libertine', Georgia, serif;",
' font-size: 13px;',
' color: #202122;',
' padding: 0;',
' overflow: hidden;',
' max-height: 340px;',
' display: flex;',
' flex-direction: column;',
'}',
'.sk-popup__header {',
' display: flex;',
' justify-content: space-between;',
' align-items: center;',
' padding: 8px 12px 6px;',
' background: #eaf3fb;',
' border-bottom: 1px solid #c8dff0;',
' flex-shrink: 0;',
'}',
'.sk-popup__title {',
' font-weight: 700;',
' font-size: 12px;',
' letter-spacing: 0.03em;',
' text-transform: uppercase;',
' color: #2563a8;',
'}',
'.sk-popup__total {',
' font-size: 12px;',
' font-weight: 600;',
' color: #555;',
' background: #fff;',
' border: 1px solid #c8dff0;',
' border-radius: 8px;',
' padding: 1px 7px;',
'}',
'.sk-popup__doi {',
' padding: 5px 12px;',
' font-size: 10.5px;',
' color: #555;',
" font-family: 'Courier New', Courier, monospace;",
' background: #fafafa;',
' border-bottom: 1px solid #e8e8e8;',
' word-break: break-all;',
' flex-shrink: 0;',
'}',
'.sk-popup__list {',
' list-style: none;',
' margin: 0;',
' padding: 0;',
' overflow-y: auto;',
' flex: 1 1 auto;',
'}',
'.sk-popup__item {',
' display: flex;',
' gap: 10px;',
' align-items: flex-start;',
' padding: 9px 12px;',
' border-bottom: 1px solid #f0f0f0;',
'}',
'.sk-popup__item:last-child { border-bottom: none; }',
'.sk-popup__thumb {',
' flex-shrink: 0;',
' width: 56px;',
' height: 56px;',
' object-fit: cover;',
' border-radius: 4px;',
' background: #eaf3fb;',
' display: block;',
'}',
'.sk-popup__text { flex: 1; min-width: 0; }',
'.sk-popup__article-title {',
' display: block;',
' color: #2563a8;',
' text-decoration: none;',
' font-weight: 600;',
' font-size: 13px;',
' line-height: 1.3;',
' margin-bottom: 3px;',
'}',
'.sk-popup__article-title:hover { text-decoration: underline; }',
'.sk-popup__snippet {',
' margin: 0;',
' font-size: 11.5px;',
' color: #555;',
' line-height: 1.45;',
' overflow: hidden;',
' display: -webkit-box;',
' -webkit-line-clamp: 3;',
' -webkit-box-orient: vertical;',
'}',
'.sk-popup__more {',
' display: block;',
' padding: 7px 12px;',
' background: #f5f9fd;',
' border-top: 1px solid #c8dff0;',
' color: #2563a8;',
' font-size: 12px;',
' font-weight: 600;',
' text-decoration: none;',
' text-align: center;',
' flex-shrink: 0;',
'}',
'.sk-popup__more:hover { background: #ddeefa; text-decoration: underline; }',
'.sk-popup__empty {',
' padding: 12px 14px;',
' color: #777;',
' font-size: 12.5px;',
' margin: 0;',
'}',
'.sk-src__favicon {',
' display: block !important;',
' width: 14px !important;',
' height: 14px !important;',
' border-radius: 2px !important;',
' object-fit: contain !important;',
'}'
].join(''));
})();