<!doctype html>
<!--
DOI-based authorship assessment tool using Crossref API.
Single HTML file, to be opened in a modern browser.
By Santosh Patnaik, MD, PhD.
CC0, GPL3
-->
<html lang="en">
<head>
<meta content="same-origin">
<title>Crossref Author Check</title>
<style>
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body, div, input, textarea {
font-size: 0.9em;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
table {
border-spacing: 0;
border-collapse: collapse;
}
button.disabled {
font-size: 1em;
font-weight: bold;
color: gray;
border: 1px solid orange;
}
button.enabled {
font-size: 1em;
font-weight: bold;
border: 1px solid blue;
background-color: whitesmoke;
}
button.minor {
padding: 0.05em 0.3em 0.1em;
color: gray;
background-color: whitesmoke;
border: none;
border-radius: 10%;
font-size: 0.8em;
}
#container {
margin: auto;
width: 800px;
}
.formTr {
background-color: lightyellow;
}
.formTd {
vertical-align: top;
padding: 1em;
border: none;
}
#helpBtn, #helpCloseBtn {
float: right;
}
.headTr {
background-color: white;
vertical-align: top;
padding: 1em;
border: none;
}
.helpTr, .helpTd {
background-color: honeydew;
vertical-align: top;
padding: 1em;
border: none;
}
.input {
color: blue;
}
input:invalid:required, textarea:invalid:required {
border: 1px solid orange;
}
input:required, textarea:required {
border: 1px solid blue;
}
input.minor {
color: gray;
}
.itemTr, .itemTr td {
vertical-align: top;
padding: 0.5em;
border: none;
}
.resultTr, .resultTd {
background-color: aliceblue;
vertical-align: top;
padding: 1em;
border: none;
}
span.header {
color: gray;
font-variant: small-caps;
font-weight: bold;
}
span.minor {
color: gray;
font-size: 0.8em;
font-weight: normal;
font-variant: normal;
}
span.inform {
color: green;
font-weight: normal;
font-variant: normal;
}
span.warning {
color: red;
font-weight: normal;
font-variant: normal;
}
.tallyTdh, .tallyTr, .tallyTrh, .tallyTr td, .tallyTrh td {
vertical-align: top;
padding: 0.1em;
border: none;
}
.tallyTdh, .tallyTr td, .tallyTrh td {
padding-right: 3em;
}
</style>
</head>
<body>
<div id="container">
<table><tbody>
<!-- Heading -->
<tr id="head" class="headTr"><td colspan="2">
<span class="header">Crossref Author Check</span>
<button id="helpBtn" class="minor" title="click for help" onclick="document.getElementById('help').style.display = ''; document.getElementById('helpBtn').style.display = 'none';">Help</button>
</td></tr>
<!-- Help -->
<tr id="help" class="helpTr" style="display: none"><td class="helpTd" colspan="2">
<strong>Help</strong> <button id="helpCloseBtn" class="minor" title="click to close Help" onclick="document.getElementById('help').style.display = 'none'; document.getElementById('helpBtn').style.display = '';"> x </button>
<br />
This tool retrieves CrossRef information on article type, authorship, print date, etc. for publications specified by DOI and/or PubMed IDs (max. 100). The tool also tallies the order of authorship for a specified author. For a PubMed ID, the tool first gets its DOI from NCBI, USA, which takes about 500 ms per ID. Hover over form fields for additional help.
<br /><br />
<span class="minor">single HTML file webpage application requiring a recent browser; save webpage as HTML file to distribute; CC0, GPL3; v1.1.1, 19 Mar 2025; <a href="https://sf.net/p/crossrefauthorcheck">update?</span>
</td></tr>
<!-- Form section -->
<tr class="formTr"><td class="formTd" width="45%">
<label for="idList">*<strong>DOI or PubMed IDs</strong>:<br />separated by space, comma, or line</label>
<br />
<textarea id="idList" rows="10" cols="40" class="input" required="required" minlength="5" title="max. 100"></textarea>
</td><td class="formTd" width="55%">
<span class="minor">* required</span>
<button id="exampleBtn" class="minor" title="click to fill with example values">Example</button>
<button id="clearBtn" class="minor" title="click to clear values">Clear form</button>
<br /><br />
<strong>Author</strong>
<br />
<label for="nameLast">*Last name:</label>
<input type="text" id="nameLast" required="required" size="12" class="input" title="required; can be just the first letter; can be any letter if you don't care; can be a regex pattern like Singh|Verma" />
<label for="nameFirst"> First name:</label>
<input type="text" id="nameFirst" size="12" class="input" title="optional; can be just the first letter, or a regex pattern like Akshay|AK or ak*; don't use period/dot unless as a regex character" />
<br /><br />
<strong>Publications since</strong>
<br />
<label for="year">Year:</label>
<input type="text" id="year" size="4" class="input" placeholder="yyyy" title="optional; 4-digit format"/>
<label for="month"> Month:</label>
<input type="text" id="month" size="2" class="input" placeholder="mm" title="optional; 2-digit format" />
<input type="checkbox" id="epubTimeUse" title="if checked, online/epub instead of print publishing time will be considered" />
<label for="epubTimeUse">E-printing time</label>
<br /><br />
<!--
<label for="ncbiApiKey"><span class="minor">NCBI API key:</span></label>
<input type="text" id="ncbiApiKey" size="32" class="minor" title="optional; to possibly reduce the chance for any PubMed query issue"/>
<br />
-->
<br /><br />
<button id="submitBtn" disabled="disabled" class="disabled" title="provide at least one ID and at least the initial of a last name">Submit</button>
<span id="submitNotice" class="inform"></span>
</td></tr>
<!-- Result section -->
<tr id="result" class="resultTr" style="display: none"><td class="resultTd" colspan="2">
<strong>Result</strong>
<br /><br /><span class="inform">Publication tally by authorship order</span><br /><br />
<div id="talliedResult"></div>
<br /><span class="inform">Publications</span><br /><br />
<div id="itemizedResult"></div>
</td></tr>
</tbody></table></div>
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> -->
<script type="text/javascript">
//<![CDATA[
//// General support functionalities
// Polyfill for Promise.allSettled
if (!Promise.allSettled) {
Promise.allSettled = function (promises) {
return Promise.all(promises.map(function (promise) {
return promise.then(function (value) {
return { status: 'fulfilled', value: value };
}).catch(function (reason) {
return { status: 'rejected', reason: reason };
});
}));
};
}
// Remove duplicate array elements
let unique = (a,t={}) => a.filter(
e=>!(t[e]=e in t)
);
// Timed promise
function withTimeout(msecs, promise) {
const timeout = new Promise((resolve, reject) =>
setTimeout(() => reject(`Timeout after ${msecs} ms.`), msecs));
return Promise.race([promise, timeout]);
}
const result = document.getElementById('result');
const itemizedResult = document.getElementById('itemizedResult');
const submitBtn = document.getElementById('submitBtn');
const submitNotice = document.getElementById('submitNotice');
//// Form interface functionalities
// Clear form
function clearFill() {
document.getElementById('idList').value = '';
document.getElementById('nameFirst').value = '';
document.getElementById('nameLast').value = '';
document.getElementById('month').value = '';
document.getElementById('year').value = '';
document.getElementById('epubTimeUse').removeAttribute('checked');
// document.getElementById('ncbiApiKey').value = '';
submitNotice.textContent = '';
itemizedResult.innerHTML = '';
result.setAttribute('style', 'display:none');
handleSubmitBtn();
}
// Example fill
function exampleFill() {
// Example PubMed IDs with no DOIs: 123456, 123567, 345671
//document.getElementById('idList').value = "34247166\n24247166\n345671";
document.getElementById('idList').value = "10.1186/s12919-020-00195-z\n10.1016/j.vaccine.2014.05.047\n10.1016/j.biologicals.2019.10.001\n10.1073/pnas.2005857117\n31200881";
document.getElementById('nameFirst').value = 'M';
document.getElementById('nameLast').value = 'Bhan';
document.getElementById('month').value = '06';
document.getElementById('year').value = '2015';
// document.getElementById('ncbiApiKey').value = '';
submitNotice.textContent = '';
itemizedResult.innerHTML = '';
result.setAttribute('style', 'display:none');
handleSubmitBtn();
}
// Disable submit if required fields not filled or result awaited
function handleSubmitBtn() {
if(document.getElementById('nameLast').value.length < 1 || document.getElementById('idList').value.length < 5) {
submitBtn.setAttribute('disabled', 'disabled');
submitBtn.setAttribute('class', 'disabled');
submitBtn.setAttribute('title', 'provide at least one ID and at least the initial of last name');
} else {
submitBtn.removeAttribute('disabled');
submitBtn.removeAttribute('title');
submitBtn.setAttribute('class', 'enabled');
}
}
//// Main
// Get DOI if PubMed ID; NCBI permits max. 3 requests/sec without API key (with key: 10)
// To do: reduce restriction if NCBI API key available
const pmDelay = (msec = 500) => new Promise(resolve => setTimeout(resolve, msec));
let pmQueryNum = 0;
async function getDoi(id) {
let doi = 0;
pmQueryNum += 1;
try {
await pmDelay(pmQueryNum * 500);
const pmResp = await withTimeout(10000, fetch(
// 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&api_key=' + document.getElementById('ncbiApiKey').value + '&id=' + id, {
'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&id=' + id, {
method: 'GET',
mode: 'cors',
}));
if(pmResp.ok) {
const pmJson = await pmResp.json();
if(pmJson !== 0 && !pmJson.hasOwnProperty('error') && !pmJson.result[`${id}`].hasOwnProperty('error')) {
doi = pmJson.result[`${id}`].articleids.filter(function(v){return v.idtype == 'doi';}).pop().value;
}
}
} catch (err) {
console.log(`DOI could not be obtained for ID ${id} (${err})`);
}
return doi;
}
// Get data for DOI
async function getData(doi, id) {
let data = 0;
if(doi) {
try {
const crResp = await withTimeout(10000, fetch(
'https://api.crossref.org/works/query=' + doi, {
method: "GET",
}));
if(crResp.ok) {
const crJson = await crResp.json();
data = crJson.message;
}
} catch (err) {
console.log(`Data could not be obtained for ID ${id} (${err})`);
}
}
return {'id': id, 'doi': doi, 'data': data};
}
// Parse DOI data
function getDataValues(id, doi, data) {
// Authors
let authors = data.author;
let all_authors = [];
for(let x in authors) {
if(!('given' in authors[x])) {
all_authors.push(authors[x]['family']).trim().replace(',', ' ').replace('\s{2,}', ' ');
} else if(authors[x]['sequence'] == 'first') {
all_authors.push(authors[x]['given'].trim().replace(',', ' ').replace('\s{2,}', ' ') + ' ' + authors[x]['family'].trim().replace(',', ' ').replace('\s{2,}', ' '));
} else if(authors[x]['sequence'] == 'additional') {
all_authors.push(authors[x]['given'].trim().replace(',', ' ').replace('\s{2,}', ' ') + ' ' + authors[x]['family'].trim().replace(',', ' ').replace('\s{2,}', ' '));
}
}
// Dates
let print_dateY = '?';
if(data['published-print'] && (print_dateY = data['published-print']['date-parts'][0][0])) {
} else if(data['journal-issue'] && (print_dateY = data['journal-issue']['published-print']['date-parts'][0][0])) {
}
let print_dateM = '?';
if(data['published-print'] && (print_dateM = data['published-print']['date-parts'][0][1])) {
} else if(data['journal-issue'] && (print_dateM = data['journal-issue']['published-print']['date-parts'][0][1])) {
}
print_date = print_dateY + '.' + (('0' + print_dateM).slice(-2));
let eprint_date = '?/?';
if(data['published-online'] && (edateInfo = data['published-online']['date-parts'][0])) {
eprint_date = (edateInfo[0]? edateInfo[0] : '?') + '.' + (edateInfo[1]? (('0' + edateInfo[1]).slice(-2)) : '?');
}
// Publication
let publication = '';
if(publicationAr = data['short-container-title']) {
publication = publicationAr.pop();
} else if(publicationAr = data['container-title']) {
publication = publicationAr.pop();
}
publication = publication?.length == 0 ? 'Unknown publication' : publication.replace('/journal/gi', 'J.');
/* Being deprecated in API
// Subject
let subject = [];
if(subjectAr = data.subject) {
subject = unique(subjectAr).filter(a=>a);
}
subject = subject?.length == 0 ? ['Unknown subject'] : subject;
*/
// Title
let title = '';
if(titleAr = data.title) {
title = titleAr.pop();
} else if(titleAr = data['short-title']) {
title = titleAr.pop();
}
title = title?.length == 0 ? 'Unknown title' : title;
// Type
let type = '';
type = data?.type;
type = type?.length == 0 ? 'unknown-type' : type;
// URL
let url = '';
if(url = data.resource?.primary?.URL) {
} else if(urlAr = data.link) {
url = urlAr.pop().URL;
}
url = url?.length !== 0 ? url : 'https://dx.doi.org/' + doi;
return {'id': id, 'doi': doi, 'all_authors': all_authors, 'eprint_date': eprint_date, 'print_date': print_date, 'publication': publication, 'title': title, 'type': type, 'url': url};
}
// Process input IDs, converting to DOIs as needed, and get Crossref data for the DOIs
async function processIds() {
submitNotice.textContent = '';
itemizedResult.innerHTML = '';
result.setAttribute('style', 'display:none');
// Get ID values from HTML textarea named idList;
// input values may be separated by space, comma, or line.
let idAr = document.getElementById('idList').value;
idAr = idAr.replace(/[\r\n\t,]/g, ' ').replace(/ +/g, ' ').split(' ');
// Sanitize IDs (e.g., remove duplicate, null, and 'doi:') and keep max. 100.
idAr = idAr.map(function(id) {
id = String(id).replace(/[^a-zA-Z0-9\-\._;()\/#%<>&;:]/g, '').replace(/^[^0-9]+/, '');
return (id.length < 5 || id.length > 500) ? null : id;
});
idAr = unique(idAr).filter(a=>a).slice(0, 100);
submitBtn.setAttribute('disabled', 'disabled');
submitBtn.setAttribute('class', 'disabled');
if(document.getElementById('nameLast').value.length < 1 || idAr.length < 1) {
submitNotice.setAttribute('class', 'warning');
submitNotice.textContent = 'Provide at least one ID and a last name!';
return 0;
} else {
submitNotice.setAttribute('class', 'inform');
submitNotice.textContent = 'Processing ' + idAr.length + ' unique input ID' + (idAr.length > 1 ? 's' : '') + '...';
}
// Get Crossref data
let todoAr = [];
let dataAr = [];
for(const id of idAr){
if(id.match(/^[0-9]+$/)) {
todo = getDoi(id)
.then(doi => {
return getData(doi, id)})
.then(data => {
dataAr.push(data);
return data;});
} else {
todo = getData(id, id)
.then(data => {
dataAr.push(data);
return data;});
}
todoAr.push(todo);
}
let results = await Promise.allSettled(todoAr);
// Initiate Crossref data display
showResult(dataAr);
}
async function showResult(dataAr) {
result.setAttribute('style', 'display:');
submitBtn.removeAttribute('disabled');
submitBtn.setAttribute('class', 'enabled');
let emptyDataAr = await dataAr.filter((v) => v.data === 0);
let itemAr = await dataAr.filter((v) => v.data !== 0);
if(!emptyDataAr.length && itemAr.length) {
submitNotice.setAttribute('class', 'inform');
submitNotice.textContent = 'Retrieved DOI info. for ' + itemAr.length + ' unique input ID' + (itemAr.length > 1 ? 's' : '');
} else if(emptyDataAr.length) {
submitNotice.setAttribute('class', 'warning');
submitNotice.textContent = 'DOI info. retrieval failed for ' + emptyDataAr.length + ' ID' + (emptyDataAr.length > 1 ? 's' : '') + '.';
}
let tallyAr = Array.apply(null, Array(16)).map(function(){return 0});
let uniqueItemAr = [];
let duplicatedItemAr = [];
let nameFirst = document.getElementById('nameFirst').value.trim().replace(',', '');
if(!nameFirst.length) {
nameFirst = '[^, ]*';
}
let nameLast = document.getElementById('nameLast').value.trim().replace(',', '');
let nameRegex = new RegExp('(( $))', 'g');
if(nameLast.length) {
nameRegex = new RegExp('(^|, )(' + nameFirst + ')([^,]*?)(' + nameLast + ')(,|$)', 'gi');
}
let testPubDate = 0;
let timepoint = 0;
let timepointYear = document.getElementById('year').value.trim();
if(/^[0-9]{4}$/.test(timepointYear)) {
let timepointMonth = document.getElementById('month').value.trim();
if(/^[0-1]*[0-9]$/.test(timepointMonth) == false) {
timepointMonth = '1';
}
timepoint = timepointYear + '.' + ('0' + timepointMonth).slice(-2);
timepoint = +timepoint;
testPubDate = document.getElementById('epubTimeUse').checked ? 2 : 1;
}
// Itemized result table
let itemizedResultTable = '<table>';
itemAr.forEach((item) => {
let values = getDataValues(item.id, item.doi, item.data);
if(uniqueItemAr.includes(item.doi)) {
duplicatedItemAr.push(item.id);
} else {
uniqueItemAr.push(item.doi);
itemizedResultTable += '<tr class="itemTr"><td><span class="inform">' + uniqueItemAr.length + '</span></td><td>';
if(values.id.match(/^[0-9]+$/)) {
itemizedResultTable += 'PMID <a href="https://pubmed.ncbi.nlm.nih.gov/' + values.id + '" onclick="window.open(\'https://pubmed.ncbi.nlm.nih.gov/' + values.id + '\'); return false">' + values.id + '</a><br />';
}
itemizedResultTable += '<a href="' + values.url + '" onclick="window.open(\'' + values.url + '\'); return false">' + values.doi + '</a><br />';
itemizedResultTable += '<em>' + values.publication + '</em><br />';
itemizedResultTable += values.type + '<br />';
let printDate = values.print_date;
let eprintDate = values.eprint_date;
let pubDate = testPubDate > 1 ? values.eprint_date : values.print_date;
let pubDateKnown = /^[0-9]{4}[.][0-9]{2}$/.test(pubDate) ? 1 : 0;
let pubDateOk = 0;
if(testPubDate && pubDateKnown && timepoint <= pubDate) {
pubDateOk = 1;
}
itemizedResultTable += (pubDateOk && testPubDate == 1 ? '<strong>' : '') + printDate.replace('.', '/') + (pubDateOk && testPubDate == 1 ? '</strong>' : '') + ' (e-pub ' + (pubDateOk && testPubDate == 2 ? '<strong>' : '') + eprintDate.replace('.', '/') + (pubDateOk && testPubDate == 2 ? '</strong>' : '') + ')<br />';
let authorOrder = values.all_authors.findIndex(value => nameRegex.test(value)) + 1;
let totalAuthors = values.all_authors.length;
let authorType = 'Not author';
if(authorOrder == 1) {
authorType = 'First';
tallyAr[1]++;
if(pubDateOk) tallyAr[6]++;
else tallyAr[11]++;
}
else if(authorOrder == totalAuthors) {
authorType = 'Last';
tallyAr[2]++;
if(pubDateOk) tallyAr[7]++;
else tallyAr[12]++;
}
else if(authorOrder == 2) {
authorType = 'Second';
tallyAr[3]++;
if(pubDateOk) tallyAr[8]++;
else tallyAr[13]++;
}
else if(authorOrder > 2) {
authorType = 'Other';
tallyAr[4]++;
if(pubDateOk) tallyAr[9]++;
else tallyAr[14]++;
}
else {
tallyAr[5]++;
if(pubDateOk) tallyAr[10]++;
else tallyAr[15]++;
}
itemizedResultTable += '</td><td>' + values.all_authors.join(', ').trim().replace(nameRegex, '$1<strong>$2$3$4 [' + authorOrder + ']</strong>');
itemizedResultTable += '</td><td>' + values.title + '</td></tr>';
}
});
itemizedResultTable += '</table>';
if(duplicatedItemAr.length != 0) {
itemizedResultTable += '<br />' + duplicatedItemAr.length + ' input ID' + (duplicatedItemAr.length > 1 ? 's appear' : ' appears') + ' to be duplicate, already covered in above result: ' + duplicatedItemAr.join(', ') + '.';
}
if(emptyDataAr.length != 0) {
itemizedResultTable += '<br /><span class="warning">Retrieval failed for ' + emptyDataAr.length + ' input ID' + (emptyDataAr.length > 1 ? 's' : '') + ': ' + emptyDataAr.map(i => {return i.id}).join(', ') + '. Check that the ID' + (emptyDataAr.length > 1 ? 's are' : ' is') + ' correct and retry.';
}
itemizedResult.innerHTML = itemizedResultTable;
// Tallied result table
let talliedResultTable = '<table>';
talliedResultTable += '<tr class="tallyTrh">' + (timepoint ? '<td class="tallyTdh"></td>' : '') + '<td>First</td><td>Last</td><td>Second</td><td>Other</td><td>Not author</td></tr>';
talliedResultTable += '<tr class="tallyTr">' + (timepoint ? '<td class="tallyThr">Any time</td>' : '') + '<td>' + tallyAr[1] + '</td><td>' + tallyAr[2] + '</td><td>' + tallyAr[3] + '</td><td>' + tallyAr[4] + '</td><td>' + tallyAr[5] + '</td></tr>';
talliedResultTable += timepoint ? '<tr class="tallyTr"><td class="tallyTdh">On/after ' + ('' + timepoint).replace('.', '/') + '</td><td>' + tallyAr[6] + '</td><td>' + tallyAr[7] + '</td><td>' + tallyAr[8] + '</td><td>' + tallyAr[9] + '</td><td>' + tallyAr[10] + '</td></tr>' : '';
talliedResultTable += timepoint ? '<tr class="tallyTr"><td class="tallyTdh">Time unknown</td><td>' + tallyAr[11] + '</td><td>' + tallyAr[12] + '</td><td>' + tallyAr[13] + '</td><td>' + tallyAr[14] + '</td><td>' + tallyAr[15] + '</td></tr>' : '';
talliedResult.innerHTML = talliedResultTable + '</table>';
}
// In case of reload with filled values
handleSubmitBtn();
document.getElementById('clearBtn').addEventListener('click', clearFill, false);
document.getElementById('exampleBtn').addEventListener('click', exampleFill, false);
submitBtn.addEventListener('click', processIds, false);
document.getElementById('idList').addEventListener('keyup', handleSubmitBtn, false);
document.getElementById('nameLast').addEventListener('keyup', handleSubmitBtn, false);
//]]>
</script>
</body>
</html>