Created
June 9, 2018 12:15
-
-
Save GaurangTandon/75af82646b40be43a368860dcf22f43d to your computer and use it in GitHub Desktop.
beta version of Citation Helper for StackExchange userscript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Citation Helper for StackExchange | |
// @description Helps insert citations easily on StackExchange | |
// @author Gaurang Tandon | |
// @match *://*.askubuntu.com/* | |
// @match *://*.mathoverflow.net/* | |
// @match *://*.serverfault.com/* | |
// @match *://*.stackapps.com/* | |
// @match *://*.stackexchange.com/* | |
// @match *://*.stackoverflow.com/* | |
// @match *://*.superuser.com/* | |
// @exclude *://api.stackexchange.com/* | |
// @exclude *://blog.stackexchange.com/* | |
// @exclude *://blog.stackoverflow.com/* | |
// @exclude *://chat.stackexchange.com/* | |
// @exclude *://chat.stackoverflow.com/* | |
// @exclude *://data.stackexchange.com/* | |
// @exclude *://elections.stackexchange.com/* | |
// @exclude *://openid.stackexchange.com/* | |
// @exclude *://stackexchange.com/* | |
// @exclude *://*/review | |
// @grant none | |
// @version 0.1 | |
// @history 0.1 Hello world! | |
// ==/UserScript== | |
// jshint -W014 | |
// https://repl.it/repls/CloudyFearfulTrust | |
// https://repl.it/repls/QuickImmenseActionscript | |
// https://repl.it/repls/DiscretePastelErrors <-- latest working | |
/** | |
* PROBLEMS: | |
this is missing the date_parts property (the one i'm using) | |
http://pubs.rsc.org/en/content/articlelanding/2016/cc/c5cc08252h | |
https://doi.org/10.1002/recl.1964083121 | |
issue raised - https://github.com/CrossRef/rest-api-doc/issues/381 | |
1. pressing enter automatically submits my form to submit Short citation, also, it is hijacking the enter key on title and edit summary fields | |
awaiting response - https://stackoverflow.com/questions/50771160/prevent-onclick-event-on-a-button-from-firing-when-hitting-enter-in-a-different | |
*/ | |
//https://github.com/LeaVerou/awesomplete/blob/gh-pages/awesomplete.min.js | |
// Awesomplete - Lea Verou - MIT license | |
!function(){var t=function(e,i){var s=this;t.count=(t.count||0)+1,this.count=t.count,this.isOpened=!1,this.input=n(e),this.input.setAttribute("autocomplete","off"),this.input.setAttribute("aria-owns","awesomplete_list_"+this.count),this.input.setAttribute("role","combobox"),this.options=i=i||{},function(t,e,i){for(var n in e){var s=e[n],r=t.input.getAttribute("data-"+n.toLowerCase());"number"==typeof s?t[n]=parseInt(r):!1===s?t[n]=null!==r:s instanceof Function?t[n]=null:t[n]=r,t[n]||0===t[n]||(t[n]=n in i?i[n]:s)}}(this,{minChars:2,maxItems:10,autoFirst:!1,data:t.DATA,filter:t.FILTER_CONTAINS,sort:!1!==i.sort&&t.SORT_BYLENGTH,container:t.CONTAINER,item:t.ITEM,replace:t.REPLACE,tabSelect:!1},i),this.index=-1,this.container=this.container(e),this.ul=n.create("ul",{hidden:"hidden",role:"listbox",id:"awesomplete_list_"+this.count,inside:this.container}),this.status=n.create("span",{className:"visually-hidden",role:"status","aria-live":"assertive","aria-atomic":!0,inside:this.container,textContent:0!=this.minChars?"Type "+this.minChars+" or more characters for results.":"Begin typing for results."}),this._events={input:{input:this.evaluate.bind(this),blur:this.close.bind(this,{reason:"blur"}),keydown:function(t){var e=t.keyCode;s.opened&&(13===e&&s.selected?(t.preventDefault(),s.select()):9===e&&s.selected&&s.tabSelect?s.select():27===e?s.close({reason:"esc"}):38!==e&&40!==e||(t.preventDefault(),s[38===e?"previous":"next"]()))}},form:{submit:this.close.bind(this,{reason:"submit"})},ul:{mousedown:function(t){t.preventDefault()},click:function(t){var e=t.target;if(e!==this){for(;e&&!/li/i.test(e.nodeName);)e=e.parentNode;e&&0===t.button&&(t.preventDefault(),s.select(e,t.target))}}}},n.bind(this.input,this._events.input),n.bind(this.input.form,this._events.form),n.bind(this.ul,this._events.ul),this.input.hasAttribute("list")?(this.list="#"+this.input.getAttribute("list"),this.input.removeAttribute("list")):this.list=this.input.getAttribute("data-list")||i.list||[],t.all.push(this)};function e(t){var e=Array.isArray(t)?{label:t[0],value:t[1]}:"object"==typeof t&&"label"in t&&"value"in t?t:{label:t,value:t};this.label=e.label||e.value,this.value=e.value}t.prototype={set list(t){if(Array.isArray(t))this._list=t;else if("string"==typeof t&&t.indexOf(",")>-1)this._list=t.split(/\s*,\s*/);else if((t=n(t))&&t.children){var e=[];i.apply(t.children).forEach(function(t){if(!t.disabled){var i=t.textContent.trim(),n=t.value||i,s=t.label||i;""!==n&&e.push({label:s,value:n})}}),this._list=e}document.activeElement===this.input&&this.evaluate()},get selected(){return this.index>-1},get opened(){return this.isOpened},close:function(t){this.opened&&(this.ul.setAttribute("hidden",""),this.isOpened=!1,this.index=-1,this.status.setAttribute("hidden",""),n.fire(this.input,"awesomplete-close",t||{}))},open:function(){this.ul.removeAttribute("hidden"),this.isOpened=!0,this.status.removeAttribute("hidden"),this.autoFirst&&-1===this.index&&this.goto(0),n.fire(this.input,"awesomplete-open")},destroy:function(){if(n.unbind(this.input,this._events.input),n.unbind(this.input.form,this._events.form),!this.options.container){var e=this.container.parentNode;e.insertBefore(this.input,this.container),e.removeChild(this.container)}this.input.removeAttribute("autocomplete"),this.input.removeAttribute("aria-autocomplete");var i=t.all.indexOf(this);-1!==i&&t.all.splice(i,1)},next:function(){var t=this.ul.children.length;this.goto(this.index<t-1?this.index+1:t?0:-1)},previous:function(){var t=this.ul.children.length,e=this.index-1;this.goto(this.selected&&-1!==e?e:t-1)},goto:function(t){var e=this.ul.children;this.selected&&e[this.index].setAttribute("aria-selected","false"),this.index=t,t>-1&&e.length>0&&(e[t].setAttribute("aria-selected","true"),this.status.textContent=e[t].textContent+", list item "+(t+1)+" of "+e.length,this.input.setAttribute("aria-activedescendant",this.ul.id+"_item_"+this.index),this.ul.scrollTop=e[t].offsetTop-this.ul.clientHeight+e[t].clientHeight,n.fire(this.input,"awesomplete-highlight",{text:this.suggestions[this.index]}))},select:function(t,e){if(t?this.index=n.siblingIndex(t):t=this.ul.children[this.index],t){var i=this.suggestions[this.index];n.fire(this.input,"awesomplete-select",{text:i,origin:e||t})&&(this.replace(i),this.close({reason:"select"}),n.fire(this.input,"awesomplete-selectcomplete",{text:i}))}},evaluate:function(){var t=this,i=this.input.value;i.length>=this.minChars&&this._list&&this._list.length>0?(this.index=-1,this.ul.innerHTML="",this.suggestions=this._list.map(function(n){return new e(t.data(n,i))}).filter(function(e){return t.filter(e,i)}),!1!==this.sort&&(this.suggestions=this.suggestions.sort(this.sort)),this.suggestions=this.suggestions.slice(0,this.maxItems),this.suggestions.forEach(function(e,n){t.ul.appendChild(t.item(e,i,n))}),0===this.ul.children.length?(this.status.textContent="No results found",this.close({reason:"nomatches"})):(this.open(),this.status.textContent=this.ul.children.length+" results found")):(this.close({reason:"nomatches"}),this.status.textContent="No results found")}},t.all=[],t.FILTER_CONTAINS=function(t,e){return RegExp(n.regExpEscape(e.trim()),"i").test(t)},t.FILTER_STARTSWITH=function(t,e){return RegExp("^"+n.regExpEscape(e.trim()),"i").test(t)},t.SORT_BYLENGTH=function(t,e){return t.length!==e.length?t.length-e.length:t<e?-1:1},t.CONTAINER=function(t){return n.create("div",{className:"awesomplete",around:t})},t.ITEM=function(t,e,i){var s=""===e.trim()?t:t.replace(RegExp(n.regExpEscape(e.trim()),"gi"),"<mark>$&</mark>");return n.create("li",{innerHTML:s,"aria-selected":"false",id:"awesomplete_list_"+this.count+"_item_"+i})},t.REPLACE=function(t){this.input.value=t.value},t.DATA=function(t){return t},Object.defineProperty(e.prototype=Object.create(String.prototype),"length",{get:function(){return this.label.length}}),e.prototype.toString=e.prototype.valueOf=function(){return""+this.label};var i=Array.prototype.slice;function n(t,e){return"string"==typeof t?(e||document).querySelector(t):t||null}function s(t,e){return i.call((e||document).querySelectorAll(t))}function r(){s("input.awesomplete").forEach(function(e){new t(e)})}n.create=function(t,e){var i=document.createElement(t);for(var s in e){var r=e[s];if("inside"===s)n(r).appendChild(i);else if("around"===s){var o=n(r);o.parentNode.insertBefore(i,o),i.appendChild(o),null!=o.getAttribute("autofocus")&&o.focus()}else s in i?i[s]=r:i.setAttribute(s,r)}return i},n.bind=function(t,e){if(t)for(var i in e){var n=e[i];i.split(/\s+/).forEach(function(e){t.addEventListener(e,n)})}},n.unbind=function(t,e){if(t)for(var i in e){var n=e[i];i.split(/\s+/).forEach(function(e){t.removeEventListener(e,n)})}},n.fire=function(t,e,i){var n=document.createEvent("HTMLEvents");n.initEvent(e,!0,!0);for(var s in i)n[s]=i[s];return t.dispatchEvent(n)},n.regExpEscape=function(t){return t.replace(/[-\\^$*+?.()|[\]{}]/g,"\\$&")},n.siblingIndex=function(t){for(var e=0;t=t.previousElementSibling;e++);return e},"undefined"!=typeof self&&(self.Awesomplete=t),"undefined"!=typeof Document&&("loading"!==document.readyState?r():document.addEventListener("DOMContentLoaded",r)),t.$=n,t.$$=s,"undefined"!=typeof self&&(self.Awesomplete=r)}(); | |
var LS_KEY = "cachedEntries", J_KEY = "journalList"; | |
if(!localStorage.getItem(LS_KEY)) localStorage.setItem(LS_KEY, "{}"); | |
(function() { | |
(function handleModals() { | |
var PLACEHOLDER = "reference (DOI/URL/plain text)"; | |
function addButton(ul) { | |
var li = document.createElement("LI"), | |
// different userscripts insert their own elements into the mix | |
// so make sure to insert just after the redo button for consistency | |
lastChild = ul.querySelector("li[id^=wmd-redo-button]").nextElementSibling; | |
ul.insertBefore(li, lastChild); | |
li.className = "wmd-button wmd-doi tmAdded"; | |
// tmAdded required when also running this userscript https://github.com/BrockA/SE-misc | |
li.innerHTML = "<span>doi</span>"; | |
li.title = "insert doi"; | |
li.onclick = function () { | |
toggleModal(ul.parentElement); | |
}; | |
return li; | |
} | |
function createModal(container) { | |
var div = document.createElement("div"), | |
input = document.createElement("input"), | |
shortBtn = document.createElement("button"), | |
longBtn = document.createElement("button"); | |
div.id = "doi-box"; | |
input.type = "text"; | |
input.className = "awesomplete"; | |
input.setAttribute("placeholder", PLACEHOLDER); | |
input.onkeydown = function(e){ | |
if(e.keyCode === 13){ | |
e.preventDefault(); | |
e.stopPropagation(); | |
console.log("I tried!"); | |
return false; | |
} | |
}; | |
shortBtn.innerHTML = "Short"; | |
longBtn.innerHTML = "Long"; | |
function commonInsertCitation(type){ | |
var fn = type === 1 ? insertShortCitation : insertLongCitation; | |
return function(e){ | |
e.preventDefault(); | |
TYPE = type; | |
var val = input.value, source = getSource(val); | |
switch(source[0]){ | |
case "doi": case "paperWeb": | |
citeDOI(source[1], function (citation) { | |
fn(container, citation); | |
}); | |
break; | |
case "web": | |
citeWebsite(val, function(citation){ | |
fn(container, citation); | |
}); | |
// it is not a DOI and there is no need to cache it (because it isn't fetched via an XHR) | |
// but adding it to the localStorage list helps autocomplete it later | |
// (think of a person referencing Vogel multiple times) | |
cacheDOI(val, val); | |
break; | |
// alternate of Manual citation | |
default: | |
cacheDOI(val, val); | |
fn(container, val); | |
} | |
}; | |
} | |
shortBtn.onclick = commonInsertCitation(1); | |
longBtn.onclick = commonInsertCitation(2); | |
div.appendChild(input); | |
div.appendChild(shortBtn); | |
div.appendChild(longBtn); | |
return div; | |
} | |
function insertShortCitation(container, citation) { | |
var textarea = container.parentNode.querySelector("textarea"), | |
selS = textarea.selectionStart, | |
selE = textarea.selectionEnd, | |
value = textarea.value, | |
valBefore = value.substring(0, selS), | |
valMid = value.substring(selS, selE), | |
valAfter = value.substring(selE); | |
textarea.value = valBefore + citation + valAfter; | |
textarea.selectionStart = textarea.selectionEnd = (valBefore + citation).length; | |
toggleModal(container); | |
textarea.focus(); | |
// couldn't find the documentation for this, but it works ---v | |
// (widely used https://github.com/search?q=refreshAllPreviews&type=Code) | |
StackExchange.MarkdownEditor.refreshAllPreviews(); | |
} | |
function getCurrentReferenceCount(value){ | |
var match = value.match(/Reference(.|\n)+(\d)\. [a-zA-Z]/); | |
// without the ` [a-zA-Z]`, this match also extends to even digits inside DOI URLs | |
if(!match) return 0; | |
else return +match[2]; | |
} | |
function insertLongCitation(container, citation) { | |
var textarea = container.parentNode.querySelector("textarea"), | |
selS = textarea.selectionStart, | |
selE = textarea.selectionEnd, | |
value = textarea.value, | |
currRefCount = getCurrentReferenceCount(value), | |
superscriptedCite = "<sup>\\[" + (currRefCount + 1) + "\\]</sup>", | |
valBefore = value.substring(0, selS), | |
valMid = value.substring(selS, selE), | |
valAfter = value.substring(selE); | |
value = valBefore + superscriptedCite + valAfter; | |
if(currRefCount === 0){ | |
value += "\n### References:\n\n1. " + citation; | |
textarea.value = value; | |
}else{ | |
var position = value.match(/Reference(.|\n)+\d\..+(\n|$)/), | |
startOfReferences = position.index, | |
lastReferenceNewline = startOfReferences + position[0].length, | |
textBeforeLastRefNewLine = value.substring(0, lastReferenceNewline), | |
textAfterLastRefNewLine = value.substring(lastReferenceNewline), | |
valToInsert = "\n" + (currRefCount + 1) + ". " + citation + "\n", | |
newValue = textBeforeLastRefNewLine + valToInsert + textAfterLastRefNewLine; | |
textarea.value = newValue; | |
} | |
textarea.selectionStart = textarea.selectionEnd = (valBefore + superscriptedCite).length; | |
toggleModal(container); | |
textarea.focus(); | |
StackExchange.MarkdownEditor.refreshAllPreviews(); | |
} | |
function toggleModal(container) { | |
var div = container.querySelector("#doi-box"), | |
input = div.querySelector("input"); | |
if (div.style.height === "0px") { | |
div.style.height = "45px"; | |
input.value = ""; | |
input.focus(); | |
} | |
else { | |
div.style.height = "0px"; | |
} | |
console.trace(); | |
} | |
var cachedKeys = Object.keys(JSON.parse(localStorage.getItem(LS_KEY))), | |
cachedKeysString; | |
if(cachedKeys.length !== 0){ | |
cachedKeysString = cachedKeys[0]; | |
for(var i = 1, len = cachedKeys.length; i < len; i++) cachedKeysString += ", " + cachedKeys[i]; | |
} | |
setInterval(function () { | |
var cont = document.querySelector(".wmd-container:not(.doi-processed)"), ul, | |
div, buttonBar, input; | |
if (cont && (ul = cont.querySelector(".wmd-button-bar ul"))) { | |
addButton(ul); | |
cont.classList.add("doi-processed"); | |
buttonBar = cont.querySelector("div[id^=wmd-button-bar]"); | |
div = createModal(buttonBar); | |
div.style.height = "0px"; | |
buttonBar.appendChild(div); | |
// only call the constructor after the input element | |
// is inside the DOM | |
input = div.querySelector("input"); | |
// for some unknown reason, using the properties in the instantiation object | |
// are not working, hence use dataset | |
input.dataset.list = cachedKeysString; | |
input.dataset.minchars = 1; | |
input.dataset.maxitems = 5; | |
input.dataset.autofirst = true; | |
new Awesomplete(input); | |
} | |
}, 500); | |
// there's no way to automatically read a clipboard content | |
// think of some other UI | |
// https://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript | |
})(); | |
})(); | |
function citeWebsite(URL, callback) { | |
// I cannot retrieve author/title of website without using my own external server :( | |
// so just do the bare citation | |
var citation = URL + " (accessed ", | |
time = new Date(), | |
months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; | |
citation += months[time.getMonth()] + " " + time.getDate() + ", " + time.getFullYear() + "."; | |
callback.call(this, citation); | |
} | |
function cacheDOI(doi, metadata){ | |
var object = JSON.parse(localStorage.getItem(LS_KEY)); | |
object[doi] = metadata; | |
localStorage.setItem(LS_KEY, JSON.stringify(object)); | |
} | |
function getDOIMetaData(doi, callback) { | |
var cachedObject = JSON.parse(localStorage.getItem(LS_KEY)), cachedMetadata; | |
if(cachedObject && (cachedMetadata = cachedObject[doi])){ | |
citeDOI(doi, callback, cachedMetadata); | |
return; | |
} | |
// ISSUE: I'm supposed to send my email along with this API call, how'd I do that? | |
var xhttp = new XMLHttpRequest(); | |
xhttp.open("GET", "https://api.crossref.org/works/" + doi, true); | |
xhttp.setRequestHeader("Content-type", "text/plain"); | |
xhttp.send(); | |
xhttp.onload = function (e) { | |
var response = JSON.parse(e.srcElement.response), metadata; | |
if (response.status !== "ok") { | |
// possibly not a cross-ref DOI | |
alert("Couldn't fetch citation for DOI: " + doi + ". Please report the doi to the userscript author."); | |
return; | |
} | |
metadata = response.message; | |
cacheDOI(doi, metadata); | |
citeDOI(doi, callback, metadata); | |
}; | |
} | |
// *J. Am. Chem. Soc.* **2018,** *140,* 1855 | |
// or *J. Am. Chem. Soc.* **2018,** *140* (2), 1855 | |
// Jounral name Year, Volume (Issue), Pages | |
function shortCiteDOI(doi, metadata) { | |
var output = "[" + getTitleYearIssuePagesForCitation(metadata) + "](https://doi.org/" + doi + ")"; | |
return output; | |
} | |
// doi must be the doi (10(.\d+)+) and nothing else | |
function citeDOI(doi, callback, metadata) { | |
if (metadata === undefined) { getDOIMetaData(doi, callback); return; } | |
var output = ""; | |
console.log(metadata); | |
if (TYPE === 1) { | |
callback.call(this, shortCiteDOI(doi, metadata)); | |
return; | |
} | |
output += citeAuthors(metadata.author); | |
output += citeTitle(metadata.title[0]) + " "; | |
output += getTitleYearIssuePagesForCitation(metadata); | |
output += " [DOI: " + doi + "](https://doi.org/" + doi + ")."; | |
callback.call(this, output); | |
} | |
function getTitleYearIssuePagesForCitation(metadata){ | |
var issue = metadata.issue, | |
output = "*" + getShortJournalTitle(metadata) + "*", | |
volume = metadata.volume, page = metadata.page; | |
output += " **" + getPublishedYear(metadata); // issue: 10.1021/ci00024a006 gives back 2005, though it was published in 1995 (legacy archives) | |
// books may not have volumes (10.1007/0-306-48639-3_12) | |
if(volume){ | |
output += ",** *" + volume; | |
output += (issue && "* (" + issue + ")") + (page ? issue ? "," : "" : "."); | |
} | |
console.log(output); | |
// page numbers are absent in ACS Article ASAP service or some other papers (10.1371/journal.pone.0068486) | |
if(page) output += (volume ? " " : ",** ") + getPageRange(page) + "."; | |
console.log(output); | |
if(!page && !volume) output += ".**"; | |
return output; | |
} | |
function getPublishedYear(metadata){ | |
// the last two are not always accurate (github.com/CrossRef/rest-api-doc/issues/381) | |
var usableDateParts = metadata["published-print"] || metadata["published-online"] || metadata["created"]; | |
return usableDateParts["date-parts"][0][0]; | |
} | |
function getShortJournalTitle(metadata){ | |
// user needs to install this via a GitHub Gist | |
var journalList = localStorage.getItem(J_KEY), title = metadata["container-title"], shortTitle = metadata["short-container-title"]; | |
// fallback to sometimes inaccurate CrossRef results in case user didn't install Gist | |
// (eg: missing the short-container-title field (10.1023/A:1008989800098); incorrect short form (Tetrahedron Letters instead of Tetrahedron Lett.) | |
// fallback to unabbrev. title in case neither list has the abbrev., or in case it's a book (not a journal - 10.1007/0-306-48639-3_12) | |
return journalList ? journalList[title] : | |
shortTitle.length !== 0 ? shortTitle[0] : title; | |
} | |
function citeAuthors(authors) { | |
// there needn't be authors all the time; (10.1007/0-306-48639-3_12) | |
if(!authors || authors.length === 0) return ""; | |
var citation = ""; | |
for (var i = 0, len = authors.length; i < len; i++) { | |
// 10.1248/cpb.49.1102 has all its author names in ALL CAPS; capitalize its only first letter | |
citation += capitalizeFirstLetter(authors[i].family) + "," + getInitials(authors[i].given); | |
citation += "; "; | |
} | |
// final two characters ("; ") are unnecessary | |
var strLen = citation.length; | |
citation = citation.substring(0, strLen - 2) + " "; | |
return citation; | |
} | |
function getInitials(givenName) { | |
return givenName.split(" ").reduce((citation, name) => citation + " " + name[0] + ".", ""); | |
} | |
var toTitleCase, capitalizeFirstLetter; | |
(function caseHelpers(){ | |
// from https://github.com/ianstormtaylor/to-no-case | |
var hasSpace = /\s/; | |
var hasSeparator = /(_|-|\.|:)/; | |
var hasCamel = /([a-z][A-Z]|[A-Z][a-z])/; | |
/** | |
* Remove any starting case from a `string`, like camel or snake, but keep | |
* spaces and punctuation that may be important otherwise. | |
* | |
* @param {String} string | |
* @return {String} | |
*/ | |
function toNoCase(string) { | |
if (hasSpace.test(string)) return string.toLowerCase(); | |
if (hasSeparator.test(string)) return (unseparate(string) || string).toLowerCase(); | |
if (hasCamel.test(string)) return uncamelize(string).toLowerCase(); | |
return string.toLowerCase(); | |
} | |
/** | |
* Separator splitter. | |
*/ | |
var separatorSplitter = /[\W_]+(.|$)/g; | |
/** | |
* Un-separate a `string`. | |
* | |
* @param {String} string | |
* @return {String} | |
*/ | |
function unseparate(string) { | |
return string.replace(separatorSplitter, function (m, next) { | |
return next ? ' ' + next : ''; | |
}); | |
} | |
/** | |
* Camelcase splitter. | |
*/ | |
var camelSplitter = /(.)([A-Z]+)/g; | |
/** | |
* Un-camelcase a `string`. | |
* | |
* @param {String} string | |
* @return {String} | |
*/ | |
function uncamelize(string) { | |
return string.replace(camelSplitter, function (m, previous, uppers) { | |
return previous + ' ' + uppers.toLowerCase().split('').join(' '); | |
}); | |
} | |
// via https://github.com/ianstormtaylor/title-case-minors and https://github.com/ianstormtaylor/to-title-case | |
var minors = ['a', 'an', 'and', 'as', 'at', 'but', 'by', 'en', 'for',' from', | |
'how', 'if', 'in', 'neither', 'nor', 'of', 'on', 'only', 'onto','out', | |
'or', 'per', 'so', 'than', 'that', 'the', 'to', 'until','up', 'upon', | |
'v', 'v.', 'versus', 'vs', 'vs.', 'via', 'when', 'with', 'without', 'yet'], | |
escaped = minors.map(function(str){ | |
return String(str).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1'); | |
}), | |
minorMatcher = new RegExp('[^^]\\b(' + escaped.join('|') + ')\\b', 'ig'), | |
punctuationMatcher = /:\s*(\w)/g; | |
function toSentenceCase(string) { | |
return toNoCase(string).replace(/[a-z]/i, function (letter) { | |
return letter.toUpperCase(); | |
}).trim(); | |
} | |
toTitleCase = function(string) { | |
return toSentenceCase(string) | |
.replace(/(^|\s)(\w)/g, function (matches, previous, letter) { | |
return previous + letter.toUpperCase(); | |
}) | |
.replace(minorMatcher, function (minor) { | |
return minor.toLowerCase(); | |
}) | |
.replace(punctuationMatcher, function (letter) { | |
return letter.toUpperCase(); | |
}); | |
}; | |
capitalizeFirstLetter = function(word){ | |
return word.charAt(0) + word.substring(1).toLowerCase(); | |
}; | |
})(); | |
function isAllUpcase(string){ | |
for(var i = 0, len = string.length, ch, isLowerCaseCharacter; i < len; i++){ | |
ch = string.charAt(i); | |
// logs false for up case characters, numbers, symbols | |
isLowerCaseCharacter = ch === ch.toLowerCase() && ch !== ch.toUpperCase(); | |
if(isLowerCaseCharacter) return false; | |
} | |
return true; | |
} | |
function citeTitle(title) { | |
// some titles are received in ALL CAPS (10.1021/ja01532a066) | |
// fix them to titlecase | |
if(isAllUpcase(title)) | |
title = toTitleCase(title); | |
var len = title.length; | |
// some paper titles don't end with a . | |
// example: 10.1021/ci00024a006 | |
if (title.charAt(len - 1) !== ".") title += "."; | |
return title; | |
} | |
function getPageRange(pages){ | |
return pages.replace(/[-]/, "–"); | |
} | |
// general DOI format: http://www.doi.org/doi_handbook/2_Numbering.html#2.2.2 | |
/* | |
The following web URLs are being checked: (they follow the format specified here https://webhome.weizmann.ac.il/home/comartin/doi.html) | |
- Wiley - https://onlinelibrary.wiley.com/doi/pdf/10.1002/9780470682531.pat0081 | |
- Springer Verlag - https://link.springer.com/article/10.1007%2Fs002140050256 | |
- Elsevier (ScienceDirect) - https://www.sciencedirect.com/science/article/pii/S0013468602000476 | |
- ACS - https://pubs.acs.org/doi/abs/10.1021/ed029p167 | |
- Nature - https://www.nature.com/articles/s41586-018-0058-6 | |
- RSC - http://pubs.rsc.org/en/content/articlelanding/2011/dt/c0dt01244k/unauth#!divAbstract | |
The following websites are unsupported: | |
- American Press IDEAL (10.1006) (absorbed by Elsevier; web URLs non existent; DOIs still work) | |
- Cambridge UP - https://www.cambridge.org/core/journals/ageing-and-society/article/social-engagement-from-childhood-to-middle-age-and-the-effect-of-childhood-socioeconomic-status-on-middle-age-social-engagement-results-from-the-national-child-development-study/2512DF65E696B95F028BF9209A9FD2DA# | |
can anyone tell how to extract DOI from this URL?! --^ | |
- i can work on these later | |
10.1046 Blackwell Publishers. Details to follow. | |
10.1055 G. Thieme Verlag (Synthesis).Structure: s-YEAR-XXXXX, where XXXXX is a 5-digit article code. | |
10.1063 American Institute of Physics. The DOI of recent articles (example: http://dx.doi.org/10.1063/1.1385363) can be found on the online abstract of the paper. However, AIP offers a very convenient alternative (a kind-of "OpenURL avant-la-lettre"): http://link.aip.org/link/?jou/vol/firstpage, where "jou" is the three-letter journal abbreviation (e.g. jcp for Journal of Chemical Physics), "vol" is the volume number and "firstpage" the first page number. Example: http://link.aip.org/link/?jcp/115/2051 will link to J. Chem. Phys. 115, 2051 (2001). | |
10.1073 PNAS (Proceedings of the National Academy of Sciences [USA]). pnas.XXXXXXXXX, where XXXXXXXXX is a 9-digit manuscript number. | |
10.1074 Journal of Biological Chemistry. Structure: jbc.MXXXXXXXXX, where XXXXXXXXX is a 9-digit manuscript number. | |
10.1080 Taylor and Francis. This publisher uses 15-character PIIs like Elsevier; again, the PII can generally be found on the online abstract of the journal paper or on the first page of the printed paper. An example DOI URL is http://dx.doi.org/10.1080/002689799163172 | |
10.1083 Rockefeller University Press (e.g. Journal of Cell Biology) | |
10.1092 Laser Pages Publishing (=Israel Journal of [fill in subject]). Structure: e.g. V0Q8-T3XM-N68W-D8NL. | |
10.1093 Oxford University Press, EMBO (European Molecular Biology Organisation). Structure: emboj/aaaXXX, where emboj stands for EMBO Journal and aaaXXX is a 3-letter, 3-digit article code. | |
10.1103 American Physical Society. Example: http://dx.doi.org/10.1103/PhysRevA.68.021801. As you see, JournalName.Volume.ArticleNumber. Like for AIP, an alternative URL is given as follows: http://link.aps.org/abstract/PRA/v68/e021801. | |
10.1107 International Union of Crystallography (e.g. Acta Crystallographica series). Uses PIIs in same way as Elsevier (see above). | |
10.1126 SCIENCE magazine. Structure: science.XXXXXXX, where XXXXXXX is a 7-digit article code. | |
10.1161 American Heart Association. | |
10.1182 American Society of hematology (e.g. the journal Bloo | |
*/ | |
function getDOIFromPaperWebURL(originalURL) { | |
// remove query parameters | |
var queryMatch = originalURL.match(/[\?#]/), URL = originalURL; | |
if (queryMatch) | |
URL = URL.substring(0, queryMatch.index); | |
if (/wiley/.test(URL)) { | |
return URL.match(/10\.\d+(\.\d+)?\/.+/)[0]; | |
} else if (/springer/.test(URL)) { | |
var matcher = URL.match(/(10\.\d+(\.\d+)*?)(%2F)?(.+)/i); | |
return matcher[1] + "/" + matcher[4]; | |
} | |
else if (/sciencedirect/.test(URL)) { | |
var PII = URL.match(/pii\/(.+)\/?/i)[1], sPresent = /pii\/s/i.test(URL), doi = sPresent ? "S" : ""; | |
// the doi substring logic below requires the S to be removed if present | |
if(sPresent) PII = PII.substring(1); | |
// PII has to be split like "Sxxxx-xxxx(yy)zzzzz-c" (S can be upcase/lowercase/absent altogether) | |
doi += PII.substring(0, 4) + "-" + PII.substring(4, 8) + "(" + PII.substring(8, 10) + ")" + PII.substring(10, 15) + "-" + PII.substring(15, 16); | |
return "10.1016/" + doi; | |
} else if (/acs/.test(URL)) { | |
return URL.match(/10\.\d+(\.\d+)?\/.+/)[0]; | |
} else if (/nature/.test(URL)) { | |
return "10.1038/" + URL.match(/\/(s.+)/)[1]; | |
} else if (/rsc/.test(URL)) { | |
// rsc url may not always have unauth at its end | |
return "10.1039/" + (URL.match(/\/([\w]+)\/unAuth$/i) || URL.match(/\/([\w]+)$/))[1]; | |
}else if(/plos/.test(URL)){ // http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0068486 | |
// the id? query parameter gets stripped away in the URL | |
return "10.1371/journal.pone." + originalURL.match(/pone\.(\d+)/)[1]; | |
} | |
return null; | |
} | |
function getSource(URI) { | |
// URIs supported: "website"/"doi"/"paperWeb" | |
// "website" is any website like HyperPhysics | |
// "doi" is literally a DOI url | |
// "paperWeb" is a wiley/elsevier/nature/etc. URL | |
URI = URI.trim(); | |
var validDOIRegexes = | |
[/^DOI: ?(10\.\d+\/.+)$/i, /^(10\.\d+\/.+)$/i, | |
/^(https?:?\/?\/?)?(dx.)?doi.org\/(10\.\d+\/.+)$/i]; | |
for (var doiRegex, i = 0, len = validDOIRegexes.length; i < len; i++) { | |
doiRegex = validDOIRegexes[i]; | |
var match = URI.match(doiRegex); | |
if (match) { | |
return ["doi", match[match.length - 1]]; | |
} | |
} | |
var DOI = getDOIFromPaperWebURL(URI); | |
if (DOI) return ["paperWeb", DOI]; | |
var validWebsiteRegexes = [/^(https?:\/?\/?)?(www\.)?[\w\.]+?\.[a-z]+[\w\d\.\/]*$/]; | |
// port://www.completeDomain/path | |
for (var webRegex, j = 0, l = validWebsiteRegexes.length; j < l; j++) { | |
webRegex = validWebsiteRegexes[j]; | |
if (webRegex.test(URI)) return ["web", URI]; | |
} | |
return [null, null]; | |
} | |
// type of citation to insert; used by citeDOI | |
// short citation = 1; long citation = 2; | |
var TYPE = 1; | |
// insert css | |
var styleEl = document.createElement('style'), | |
cssToUse = | |
` | |
/**https://cdnjs.com/libraries/awesomplete*/ | |
.awesomplete [hidden]{display:none}.awesomplete .visually-hidden{position:absolute;clip:rect(0,0,0,0)}.awesomplete{display:inline-block;position:relative}.awesomplete>input{display:block}.awesomplete>ul{position:absolute;left:0;z-index:1;min-width:100%;box-sizing:border-box;list-style:none;padding:0;border-radius:.3em;margin:.2em 0 0;background:hsla(0,0%,100%,.9);background:linear-gradient(to bottom right,#fff,hsla(0,0%,100%,.8));border:1px solid rgba(0,0,0,.3);box-shadow:.05em .2em .6em rgba(0,0,0,.2);text-shadow:none}.awesomplete>ul:empty{display:none}@supports (transform:scale(0)){.awesomplete>ul{transition:.3s cubic-bezier(.4,.2,.5,1.4);transform-origin:1.43em -.43em}.awesomplete>ul:empty,.awesomplete>ul[hidden]{opacity:0;transform:scale(0);display:block;transition-timing-function:ease}}.awesomplete>ul:before{content:"";position:absolute;top:-.43em;left:1em;width:0;height:0;padding:.4em;background:#fff;border:inherit;border-right:0;border-bottom:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}.awesomplete>ul>li{position:relative;padding:.2em .5em;cursor:pointer}.awesomplete>ul>li:hover{background:#b7d2e0;color:#000}.awesomplete>ul>li[aria-selected=true]{background:#3d6c8e;color:#fff}.awesomplete mark{background:#e9ff00}.awesomplete li:hover mark{background:#b5d100}.awesomplete li[aria-selected=true] mark{background:#3c6b00;color:inherit} | |
.awesomplete{ | |
position: inherit !important; | |
/* required to keep the input element hidden while modal is collapsed*/ | |
} | |
#doi-box{ | |
transition: 0.25s ease; | |
} | |
#doi-box input{ | |
display: inline-block; | |
width: 500px; | |
font-size: 14px; | |
padding: 8px; | |
position: inherit; | |
} | |
#doi-box button{ | |
position: inherit; /*allows buttons to flow in and out*/ | |
margin: 5px; | |
} | |
`; | |
styleEl.setAttribute('type', 'text/css'); | |
styleEl.textContent = cssToUse; | |
document.head.appendChild(styleEl); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment