Skip to content

Instantly share code, notes, and snippets.

@tillahoffmann
Last active October 6, 2023 21:30
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save tillahoffmann/ac583f40d7898da410284c5b5aba8399 to your computer and use it in GitHub Desktop.
Save tillahoffmann/ac583f40d7898da410284c5b5aba8399 to your computer and use it in GitHub Desktop.
Readcube-Overleaf integration: Adds an "Update Library" button to Overleaf that allows you to import your Readcube library.
// ==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);
})();
@PaulVanSchayck
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment