ScholarKey

Customizable database access icons appear beside DOI URLs—preloaded with Anna's Archive (for books), SciDB, and Sci-Hub.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==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(''));
})();