NASA ADS Link on AAS/IOP pages (Robust DOI extraction)

Adds a floating "NASA ADS" button to IOP/AAS article pages, robustly extracting DOI even for /pdf URLs

// ==UserScript==
// @name         NASA ADS Link on AAS/IOP pages (Robust DOI extraction)
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Adds a floating "NASA ADS" button to IOP/AAS article pages, robustly extracting DOI even for /pdf URLs
// @match        https://iopscience.iop.org/article/*
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';

  const stopSegments = new Set(['pdf','abstract','full','epub','supp','supplementary','figures','references','full-text','text','download']);

  function cleanCandidate(c){
    if(!c) return null;
    c = c.split(/[?#]/)[0];            // strip query/hash
    c = c.replace(/\/+$/,'');          // strip trailing slashes
    return c;
  }

  function doiFromMeta(){
    const selectors = [
      'meta[name="citation_doi"]',
      'meta[name="dc.Identifier"]',
      'meta[name="DC.Identifier"]',
      'meta[name="dc.identifier"]',
      'meta[name="DOI"]',
      'meta[property="og:doi"]'
    ];
    for(const sel of selectors){
      const m = document.querySelector(sel);
      if(m && m.content) return cleanCandidate(m.content);
    }
    // fallback: canonical link may contain DOI
    const canonical = document.querySelector('link[rel="canonical"]');
    if(canonical && canonical.href){
      const match = canonical.href.match(/10\.\d{4,9}\/[-._;()\/:A-Z0-9]+/i);
      if(match) return cleanCandidate(match[0]);
    }
    return null;
  }

  function doiFromLinks(){
    const a = document.querySelector('a[href*="doi.org"], a[href*="/doi/"], a[href*="/article/10."]');
    if(a){
      const href = a.href;
      const match = href.match(/10\.\d{4,9}\/[-._;()\/:A-Z0-9]+/i);
      if(match) return cleanCandidate(match[0]);
    }
    return null;
  }

  function doiFromPath(path){
    if(!path) return null;
    const parts = path.split('/').filter(Boolean);
    // find 'article' segment OR first segment that starts with '10.'
    let idx = parts.indexOf('article');
    if(idx === -1) idx = parts.findIndex(p => p.startsWith('10.'));
    if(idx === -1) return null;
    const start = (parts[idx] === 'article') ? idx + 1 : idx;
    if(start >= parts.length) return null;
    const out = [];
    for(let i = start; i < parts.length; i++){
       const seg = parts[i];
       if(stopSegments.has(seg.toLowerCase())) break;
       out.push(seg);
    }
    if(out.length === 0) return null;
    const candidate = out.join('/');
    return (/10\.\d{4,9}\/\S+/.test(candidate)) ? cleanCandidate(candidate) : null;
  }

  function doiFromReferrer(){
    try{
      const ref = document.referrer;
      if(ref){
        const url = new URL(ref);
        const doi = doiFromPath(url.pathname) || (ref.match(/10\.\d{4,9}\/[-._;()\/:A-Z0-9]+/i) || [])[0];
        if(doi) return cleanCandidate(doi);
      }
    }catch(e){}
    return null;
  }

  // try sources in order of reliability
  let doi = null;
  try {
    doi = doiFromMeta() || doiFromLinks() || doiFromPath(window.location.pathname) || doiFromReferrer();
  } catch(e) {
    console.error('DOI extraction error', e);
  }

  if(!doi) return; // nothing we can do

  const adsUrl = `https://ui.adsabs.harvard.edu/abs/${doi}/abstract`;

  // floating button (bottom-right)
  const btn = document.createElement('a');
  btn.href = adsUrl;
  btn.target = '_blank';
  btn.rel = 'noopener noreferrer';
  btn.textContent = 'NASA ADS';
  Object.assign(btn.style, {
    position: 'fixed',
    bottom: '20px',
    right: '20px',
    backgroundColor: '#003366',
    color: '#fff',
    padding: '10px 14px',
    borderRadius: '8px',
    textDecoration: 'none',
    fontWeight: '600',
    zIndex: '2147483647',
    boxShadow: '0 4px 12px rgba(0,0,0,0.25)',
    fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial',
    cursor: 'pointer',
  });

  btn.addEventListener('mouseenter', ()=> btn.style.transform = 'translateY(-2px)');
  btn.addEventListener('mouseleave', ()=> btn.style.transform = 'translateY(0)');

  document.body.appendChild(btn);

})();