-
-
Save tillahoffmann/ac583f40d7898da410284c5b5aba8399 to your computer and use it in GitHub Desktop.
// ==UserScript== | |
// @name Readcube-Overleaf integration | |
// @namespace https://tillahoffmann.github.io/ | |
// @version 0.1 | |
// @description Adds an "Update Library" button to Overleaf that allows you to import your Readcube library. | |
// @author Till Hoffmann | |
// @match https://www.overleaf.com/* | |
// @connect readcube.com | |
// @grant GM_xmlhttpRequest | |
// ==/UserScript== | |
function formatPageNumbers(x) { | |
// Remove spaces | |
x = x.replaceAll(/\s/g, ''); | |
// Replace single dashes with double dashes | |
x = x.replaceAll(/(?<=\d)-(?=\d)/g, '--'); | |
return x; | |
} | |
const formattingLookup = { | |
'title': 'article/title', | |
'journal': 'article/journal', | |
'pages': { | |
'path': 'article/pagination', | |
'format': formatPageNumbers, | |
}, | |
'volume': 'article/volume', | |
'year': 'article/year', | |
'doi': 'ext_ids/doi', | |
}; | |
function formatItem(item, usedKeys) { | |
var citekey = item.user_data.citekey; | |
// Cite key is available | |
if (citekey) { | |
citekey = citekey.replaceAll("'", ""); | |
} | |
// Generate a cite key based on the author | |
else if (item.article.authors.length) { | |
var author = item.article.authors[0].split(/\s+/); | |
// Get the last name of the author (naively, anyway) | |
citekey = author[author.length - 1]; | |
// Add the year if it's available | |
if (item.article.year) { | |
citekey = citekey + item.article.year; | |
} | |
// Add a suffix if it's not unique | |
if (usedKeys[citekey]) { | |
usedKeys[citekey] += 1; | |
citekey += String.fromCharCode(95 + usedKeys[citekey]); | |
} else { | |
usedKeys[citekey] = 1; | |
} | |
} | |
// Just generate something random | |
else { | |
var randomInt = Math.floor(Math.random() * 0xffffffff); | |
citekey = randomInt.toString(16); | |
} | |
var lines = [ | |
'@' + item.item_type + '{' + citekey + ',', | |
' author = {' + (item.article.authors || []).join(' and ') + '},', | |
]; | |
for (var [key, value] of Object.entries(formattingLookup)) { | |
if (typeof value === "string") { | |
value = {'path': value}; | |
} | |
var x = item; | |
for (var subpath of value.path.split('/')) { | |
x = x[subpath]; | |
if (x === undefined) { | |
break; | |
} | |
} | |
if (x) { | |
if (value.format) { | |
x = value.format(x); | |
} | |
lines.push(' ' + key + ' = {' + x + '},'); | |
} | |
} | |
lines.push('}'); | |
return lines.join('\n'); | |
} | |
function fetchItems(config) { | |
// Construct the url | |
var url = config.baseUrl; | |
if (config.scrollId) { | |
url = url + '?scroll_id=' + config.scrollId; | |
} | |
GM_xmlhttpRequest({ | |
method: "GET", | |
url: url, | |
onload: function(response) { | |
unsafeWindow.config = config; | |
var data = JSON.parse(response.responseText); | |
if (data.items.length > 0) { | |
config.items = (config.items || []).concat(data.items); | |
// Continue recursively | |
fetchItems(Object.assign({}, config, {scrollId: data.scroll_id})); | |
} else if (config.callback) { | |
config.callback(config); | |
} | |
} | |
}); | |
} | |
function updateLibrary() { | |
// Get the first editor and fetch the code | |
var editor = null; | |
for (let element of document.getElementsByClassName('ace_editor')) { | |
editor = ace.edit(element); | |
break; | |
} | |
var code = editor.getValue(); | |
// Try to match the URL pattern and load the items | |
var pattern = /%% https?:\/\/app.readcube.com\/library\/([\w-]+)\/list\/([\w-]+)/gi; | |
for (var match of code.matchAll(pattern)) { | |
console.log('library: ', match[1]); | |
console.log('list: ', match[2]); | |
var url = 'https://sync.readcube.com/collections/' + match[1] + '/lists/' + match[2] + '/items'; | |
console.log('fetching ' + url); | |
fetchItems({ | |
'baseUrl': url, | |
'match': match, | |
'editor': editor, | |
'callback': function(config) { | |
// Format the code and add it to the editor | |
console.log(config); | |
// Sort the items for consistent cite key generation | |
config.items.sort(function(a, b) { | |
if (a.id < b.id) { | |
return -1; | |
} | |
return 1; | |
}); | |
var lines = [config.match[0]]; | |
var usedKeys = {}; | |
for (var item of config.items) { | |
lines.push(formatItem(item, usedKeys)); | |
} | |
editor.setValue(lines.join('\n\n')); | |
}, | |
}); | |
// We only process one library (for now anyway) | |
break; | |
} | |
} | |
(function() { | |
'use strict'; | |
// Inject the update button in the toolbar | |
setInterval(function() { | |
var parent = document.querySelector('.toolbar-editor .toolbar-right'); | |
if (parent.getAttribute('readcube')) { | |
return; | |
} | |
var child = document.createElement('a'); | |
child.innerText = 'Update Library'; | |
child.onclick = function() { | |
updateLibrary(); | |
} | |
parent.insertBefore(child, parent.firstChild); | |
parent.setAttribute('readcube', 'injected'); | |
}, 1000); | |
})(); |
Hi, just to let you all know, I've put together a new version of the code. Now, the imported bib entries can be sorted according to the name of the first author. This is necessary in order to have the multi-citatations with natbib properly sorted in the PDF. I've added a toggle to use this option (set as default) or not which appears after hovering over the import button for 1.5 s. Besides, a minor bug stemming from the Overleaf new text editor behaviour, which prevented re-importing bib entries if scrolled down far enough, is bypassed by automatic scroll-to-the-top in the editor before the import itself.
The revision is available in my repository: https://gist.github.com/MichalPt/0d08085321d03a63aca07720785849c8
Thanks @MichalPt . Nice workaround with the editor.
Another thing I recently added to my version is to rewrite the ReadCube type to the Bibtex type. Unfortunately, they don't always match:
function formatType(x) {
const types = {
'conference_paper' : 'inproceedings',
'poster': 'inproceedings',
'webpage': 'misc'
}
if( x in types ) {
return types[x]
} else {
return x
}
}
And call formatType(item.item_type)
I've updated my revision again. Changes:
- Merged changes from @MichalPt (scrolling to top, sorting)
- Added the formatType() function from above
- Added a formatRaw() for URL and DOI to prevent from escaping special chars in those fields.
You need to create a list. Click the gear button next to "My Papers," click "Create List," and give a name to the list. Then, assign the papers you want to cite to the list (When you click a paper, you will see there are "TAGS", "LISTS", "CITEKEY", etc. Use "LIST" to add the paper to the list). Then, use the list's URL on Overleaf. Make sure you put double percent sign (%%) in front of the URL. I hope this is useful.