<!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>