Publication Auto PDF

Automatically jumps to PDF when you visit a journal article abstract page. Also includes a utility to copy or download citation info.

// ==UserScript==
// @name    Publication Auto PDF
// @name:zh-CN  SCI文献PDF直达
// @version 0.5.0
// @author  sincostandx
// @description Automatically jumps to PDF when you visit a journal article abstract page. Also includes a utility to copy or download citation info.
// @description:zh-CN  访问SCI文献摘要页时自动跳转至PDF,附带文献摘录工具
// @match https://www.sciencedirect.com/science/article/*
// @match https://onlinelibrary.wiley.com/doi/*
// @match https://*.onlinelibrary.wiley.com/doi/*
// @match https://pubs.acs.org/doi/*
// @match https://www.tandfonline.com/doi/*
// @match https://www.beilstein-journals.org/*
// @match https://pubs.rsc.org/en/content/*
// @match https://link.springer.com/article*
// @match https://pubs.aip.org/aip/*/article/*
// @match https://www.nature.com/articles*
// @match https://www.science.org/doi/*
// @match https://journals.aps.org/*/abstract/10*
// @match https://cdnsciencepub.com/doi/*
// @match https://iopscience.iop.org/article/10*
// @match https://www.cell.com/*/fulltext/*
// @match https://journals.lww.com/*
// @match https://*.biomedcentral.com/articles/*
// @match https://journals.sagepub.com/doi/*
// @match https://academic.oup.com/*/article/*
// @match https://karger.com/*/article/*
// @match https://www.cambridge.org/core/journals/*/article/*
// @match https://www.annualreviews.org/doi/*
// @match https://www.jstage.jst.go.jp/article/*
// @match https://www.hindawi.com/journals/*
// @match https://*.theclinics.com/article/*
// @match https://www.liebertpub.com/doi/*
// @match https://thorax.bmj.com/content/*
// @match https://journals.physiology.org/doi/*
// @match https://www.ahajournals.org/doi/*
// @match https://dl.acm.org/doi/*
// @match https://journals.asm.org/doi/*
// @match https://*.apa.org/record/*
// @match https://*.apa.org/fulltext/*
// @match https://www.thelancet.com/journals/*/article/*
// @match https://jamanetwork.com/journals/*
// @match https://aacrjournals.org/*/article/*
// @match https://royalsocietypublishing.org/doi/*
// @match https://journals.plos.org/*/article*
// @match https://*.psychiatryonline.org/doi/*
// @match https://opg.optica.org/*/*.cfm*
// @match https://www.thieme-connect.de/products/ejournals/*
// @match https://journals.ametsoc.org/view/journals/*
// @match https://www.frontiersin.org/articles/*
// @match https://www.worldscientific.com/doi/*
// @match https://www.nejm.org/doi/*
// @match https://ascopubs.org/doi/*
// @match https://www.jto.org/article/*
// @match https://www.jci.org/articles/*
// @match https://www.pnas.org/doi/*
// @grant   GM.getValue
// @grant   GM.setValue
// @run-at  document-start
// @namespace https://greasyfork.org/users/171198
// ==/UserScript==

(function() {
  "use strict";

  let tit = null; // title
  let doi = null;
  let pdf = null; // pdf url

  let sty = null; // citation text style

  // attempt to extract DOI from URL
  function getCrudeDOI() {
    const l = location.pathname.match(/(^.+doi\/)([^/]+\/)?(10\.[^/]+\/[^/]+)/);
    if (l === null) return [null, null];
    const d = l[l.length-1];
    return [d, l[1] + "pdf/" + d];
  }

  const [doiCrude, pdfPathname] = getCrudeDOI();

  // determine if we need to redirect to PDF
  let jump = sessionStorage.getItem("%" + doiCrude) === null &&
                                          sessionStorage.getItem(location.pathname) === null;

  // For sites in "shortcutSites" modify the URL directly.
  // Otherwise, load PDF link from meta data or DOM.
  if (doiCrude !== null) {
    sessionStorage.setItem("%" + doiCrude, "1");
    if (jump && location.pathname !== pdfPathname) {
      const shortcutSites = ["acs", "aps", "wiley", "tandfonline", "sagepub", "annualreviews", "liebertpub",
                             "physiology", "ahajournals", "acm", "royalsocietypublishing", "psychiatryonline", "thieme",
                             "worldscientific", "nejm", "ascopubs", "cdnsciencepub", "asm", "science", "pnas"];
      const hostname = location.hostname;
      if (shortcutSites.some(a=>hostname.includes(a))) {
        location.pathname = pdfPathname;
      }
    } else {
      new Promise(checkLoaded).then(loadMeta);
    }
  } else {
    sessionStorage.setItem(location.pathname, "1");
    if (location.hostname.includes(".apa.")) {
      new Promise(checkAPALoaded).then(loadMeta);
    } else {
      new Promise(checkLoaded).then(loadMeta);
    }
  }

  function checkLoaded(resolve) {
    function check(){
      if (document.body !== null && document.body.innerHTML.length !== 0) {
        resolve();
      } else {
        setTimeout(check, 100);
      }
    }
    check();
  }

  function checkAPALoaded(resolve) {
    let count = 0;
    function check(){
      const main = document.querySelector("main");
      if (main !== null && main.offsetHeight > 300) {
        resolve();
      } else if (++count < 30){
        setTimeout(check, 1000);
      }
    }
    check();
  }

  function loadMeta() {
    const titmeta = ["dc.title", "citation_title", "wkhealth_title", "og:title"];
    const doimeta = ["citation_doi", "dc.identifier", "dc.source"];
    const pdfmeta = ["citation_pdf_url", "wkhealth_pdf_url"];
    const metaList = document.getElementsByTagName("meta");
    for (const meta of metaList) {
      let n = meta.getAttribute("name");
      if (n === null) {
        n = meta.getAttribute("property");
        if (n === null) continue;
      }
      n = n.toLowerCase();
      if (tit === null && titmeta.includes(n)) {
        tit = meta.getAttribute("content");
        continue;
      }
      if (doi === null && doimeta.includes(n)) {
        const d = meta.getAttribute("content");
        if (d.includes("10.")) {
          if (d.includes("doi")) {
            doi = d.slice(d.indexOf("10."));
          } else {
            doi = d;
          }
          continue;
        }
      }
      if (pdf === null && pdfmeta.includes(n)) {
        pdf = meta.getAttribute("content");
      }
    }
    if (jump && location.hostname.includes("sciencedirect")) {
      if (loadElsevierPDF()) return;
    }
    if (pdf !== null && location.hostname.includes(".apa.")) {
      // need to remove query string in apa pdf links
      const url = new URL(pdf);
      url.search = '';
      pdf = url.toString();
    }
    if (jump && pdf !== null && location.href !== pdf) {
      location.href = pdf;
    } else {
      if (doi === null) {
        doi = doiCrude;
        if (doi === null) return;
      }
      if (tit === null) tit = "Unknown Title";
      if (pdf === null) pdf = pdfPathname;
      toolbox(tit, doi, pdf);
    }
  }

  // newbr, newinput, newtag are util functions to create toolbox elements.
  function newbr(parent) {
    parent.appendChild(document.createElement("br"));
  }

  function newinput(parent, type, value, onclick) {
    const i = document.createElement("input");
    i.type = type;
    i.value = value;
    if (onclick !== null) {
      i.addEventListener("click", onclick, false);
    }
    i.className = "toolbox";
    parent.appendChild(i);
    return i;
  }

  function newtag(parent, tag, text) {
    const i = document.createElement(tag);
    if (text !== null) {
      i.textContent = text;
    }
    i.className = "toolbox";
    parent.appendChild(i);
    return i;
  }

  function loadElsevierPDF() {
    let pdflink = null;
    let trials = 0;
    function getJSON(resolve) {
      function get() {
        const json = document.querySelector('script[type="application/json"]');
        try {
          const meta = JSON.parse(json.innerHTML).article.pdfDownload.urlMetadata;
          pdflink = "/" + meta.path + "/" + meta.pii + meta.pdfExtension + "?md5=" + meta.queryParams.md5 + "&pid=" + meta.queryParams.pid;
          return true;
        } catch(e) {
          return false;
        }
      }
      if (!get()) {
        if (++trials > 30) {
          console.error("Auto PDF: Unable to load JSON from DOM!");
          throw "";
        }
        setTimeout(() => {getJSON(resolve);}, 1000);
        return;
      }
      resolve(pdflink);
    }
    new Promise(getJSON).then((pdflink) => {
      location.href = pdflink;
    }).catch(() => {});
    return true;
  }

  function toolbox(tit, doi, pdf) {
    const div = document.createElement("div");
    div.style = `
z-index: 2147483647;
position: fixed;
right: 10px;
top: 50%;
transform: translate(0, -50%);
border: 2px groove black;
background: white;
box-shadow: 6px 6px 3px grey;`;

    const sheet = document.createElement("style")
    sheet.textContent = `
.toolbox {
  font-size: small !important;
  font-family: sans-serif !important;
  margin-bottom: 4px !important;
  margin-top: 0 !important;
  display: initial !important;
  line-height: initial !important;
}

input.toolbox, select.toolbox {
  background-image: none !important;
  width: auto;
  max-height: none;
  height: initial;
}

textarea.toolbox, input.toolbox[type=text] {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
  width: 11em;
  padding: 0.5rem;
  border-style: solid;
}

a.toolbox:hover {
  color: #006db4;
}

a.toolbox {
  cursor: pointer;
  color: #10147e;
}

input.toolbox[type=button]:hover {
  background: #006db4;
}

input.toolbox[type=button] {
  padding: 4px 8px;
  background: #10147e;
  color: white;
  border-radius: 4px;
  border: none;
  cursor: pointer;
}

[tooltip]:before {
  position: absolute;
  opacity: 0;
}

[tooltip]:hover:before {
  content: attr(tooltip);
  opacity: 1;
  color: black;
  background: white;
  padding: 2px;
  border: 1px groove black;
  white-space: nowrap;
  overflow: hidden;
  right: 0;
  margin-top: -25pt;
}

h2.toolbox {
  font-size: large !important;
}
`;
    div.appendChild(sheet);

    // Hide and Settings button
    const hide_btn_parent = newtag(div, "div", null);
    hide_btn_parent.style = "display: flex !important; justify-content: flex-end; padding-right: 10px;";
    const hide_btn = newtag(hide_btn_parent, "a", "✖");
    hide_btn.style = "font-size: large !important; text-decoration: none;";
    hide_btn.addEventListener("click", () => div.remove(), false);

    // DOI textbox and copy button
    const txt_doi = newinput(div,"text",doi,null);
    newbr(div);
    newinput(div, "button", "Copy", () => {txt_doi.select(); document.execCommand("Copy");});
    newbr(div);newbr(div);

    // info textbox and copy button
    const info = tit + "\t" + doi;
    const txt_inf = newinput(div, "text", info, null);
    newbr(div);
    newinput(div, "button", "Copy", () => {txt_inf.select(); document.execCommand("Copy");});
    newbr(div);newbr(div);

    // bibtex button
    const txt_bib = newtag(div, "textarea", null);
    txt_bib.style = "resize: none; display: none !important;";
    newbr(div);
    const btn_bib_label = "Copy BibTeX";
    const btn_bib = newinput(div, "button", btn_bib_label, loadBib);
    newbr(div);
    function loadBib() {
      btn_bib.disabled = true;
      btn_bib.value = "Loading";
      fetch("https://dx.doi.org/" + doi, {
        headers: {
          "Accept": "application/x-bibtex"
        }})
        .then(response => response.text())
        .then(data => {
          txt_bib.value = data;
          txt_bib.style.display = "";
          txt_bib.select();
          document.execCommand("Copy");
          btn_bib.value = btn_bib_label;
          btn_bib.disabled = false;
        })
        .catch(error => {
          alert("Cannot load BibTeX.");
          btn_bib.value = btn_bib_label;
          btn_bib.disabled = false;
        });
    }
    // Text button
    const btn_txt_label = "Copy Text";
    const btn_txt = newinput(div, "button", btn_txt_label, loadTxt);
    function loadTxt() {
      if (sty === "") {
        setstyle();
        return;
      }
      btn_txt.disabled = true;
      btn_txt.value = "Loading";
      fetch("https://dx.doi.org/" + doi, {
        headers: {
          "Accept": "text/x-bibliography; style=" + sty
        }})
        .then(response => response.text())
        .then(data => {
          txt_bib.value = data;
          txt_bib.style.display = "";
          txt_bib.select();
          document.execCommand("Copy");
          btn_txt.value = btn_txt_label;
          btn_txt.disabled = false;
        })
        .catch(error => {
          alert("Cannot load text reference.");
          btn_txt.value = btn_txt_label;
          btn_txt.disabled = false;
        });
    }
    // text style link
    const stylink = newtag(div, "a", "Style");
    stylink.addEventListener("click", setstyle, false);
    newbr(div);

    function setstyle() {
      const div = document.createElement("div");
      div.style = `
z-index: 2147483647;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 2px groove black;
background: white;
padding: 20px;
box-shadow: 10px 10px 5px grey;`;

      newtag(div, "h2", "Choose a citation style for text references:");
      const sel = newtag(div, "select", null);
      sel.size = 15;
      sel.style.minWidth = "600px";
      newbr(div);
      newtag(div, "p", "This citation style will be saved as default.").style = "margin-top: 5px !important";
      newbr(div);

      const btns = newtag(div, "div", null);
      btns.style = "display: block !important; text-align: center;";
      // OK button
      newinput(btns, "button", "OK", () => {
        setStyleAndTooltip(sel.value);
        GM.setValue("sty", sty);
        document.body.removeChild(div);
        loadTxt();
      });
      // cancel button
      newinput(btns, "button", "Cancel", () => document.body.removeChild(div)).style.marginLeft = "1em";

      // load styles
      fetch("https://citation.crosscite.org/styles")
        .then(response => response.json())
        .then(data => {
          for (const i of data) {
            const x = document.createElement("option");
            x.text = i;
            sel.add(x);
          }
          if (sty !== "") {
            sel.value = sty;
          } else {
            sel.selectedIndex = 0;
          }
        })
        .catch(error => {
          alert("Cannot load style list.");
          document.body.removeChild(div);
        });
      document.body.appendChild(div);
    }

    // RIS link
    const rislink = newtag(div, "a", "Download RIS");
    rislink.addEventListener("click", loadris);
    function loadris() {
      rislink.removeEventListener("click", loadris);
      fetch("https://dx.doi.org/" + doi, {
        headers: {
          "Accept": "application/x-research-info-systems"
        }})
        .then(response => response.text())
        .then(data => {
          const blob = new Blob([data], {type: "octet/stream"});
          const url = URL.createObjectURL(blob);
          rislink.href = url;
          rislink.download = doi.replace("/", "_") + ".ris";
          rislink.click();
        })
        .catch(error => {
          alert("Cannot download RIS.");
          rislink.addEventListener("click", loadris);
        });
    }

    newbr(div);newbr(div);

    // PDF link
    if (pdf === null && location.hostname.includes("sciencedirect")) {
      pdf = sessionStorage.getItem(doi);
      if (pdf === null) {
        const pdflink = newtag(div, "a", "PDF");
        pdflink.addEventListener("click",() => {if (!loadElsevierPDF()) alert("Failed to load PDF link");});
        newbr(div);newbr(div);
      }
    }
    if (pdf !== null) {
      const pdflink = newtag(div,"a","PDF");
      pdflink.href = pdf;
      newbr(div);newbr(div);
    }

    // Sci-Hub link
    const scihubURL = "https://sci-hub.st/";
    const scihublink = newtag(div, "a", "Sci-Hub");
    if (doi !== null) {
      scihublink.href = scihubURL + doi;
    } else {
      scihublink.href = scihubURL + location.href;
    }
    newbr(div);newbr(div);

    document.body.appendChild(div);

    function setStyleAndTooltip(v) {
      sty = v;
      stylink.setAttribute("tooltip", v === null ? "Reference style not set" : "Current style: " + v);
    }

    GM.getValue("sty", null).then(setStyleAndTooltip);
  }
})();