ScholarKey

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 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(''));
})();