Skip to content

Instantly share code, notes, and snippets.

@epaaso
Forked from tillahoffmann/readleaf.user.js
Last active January 25, 2023 00:11
Show Gist options
  • Save epaaso/349f1de38867a66e7be3be8f319e3946 to your computer and use it in GitHub Desktop.
Save epaaso/349f1de38867a66e7be3be8f319e3946 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',
'url': 'custom_metadata/url'
};
const escapes = {
"{": "\\{",
"}": "\\}",
"\\": "\\textbackslash{}",
"#": "\\#",
"$": "\\$",
"%": "\\%",
"&": "\\&",
"^": "\\textasciicircum{}",
"_": "\\_",
"~": "\\textasciitilde{}",
};
// Inspired on https://github.com/dangmai/escape-latex
function escapeLatex(x) {
const escapeKeys = Object.keys(escapes);
let runningStr = String(x);
let result = "";
// Algorithm: Go through the string character by character, if it matches
// with one of the special characters then we'll replace it with the escaped
// version.
while (runningStr) {
let specialCharFound = false;
escapeKeys.forEach(function(key, index) {
if (specialCharFound) {
return;
}
if (
runningStr.length >= key.length &&
runningStr.slice(0, key.length) === key
) {
result += escapes[escapeKeys[index]];
runningStr = runningStr.slice(key.length, runningStr.length);
specialCharFound = true;
}
});
if (!specialCharFound) {
result += runningStr.slice(0, 1);
runningStr = runningStr.slice(1, runningStr.length);
}
}
return result;
}
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 && 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);
}else {
x = escapeLatex(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('cm-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));
}
var session = editor.session
session.insert({
row: session.getLength(),
column: 0
}, "\n" + 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() {
// Selecting toolbar
var parent = document.querySelector('div.toolbar-right');
if (parent.getAttribute('readcube')) {
return;
}
// Creating proper toolbar button
var sect = document.createElement('div')
sect.classList.add('toolbar-item')
// Creating button itself
var child = document.createElement('button');
//child.innerText = 'Update Library';
child.classList.add('btn', 'btn-info', 'btn-full-height');
child.onclick = function() {
updateLibrary();
}
var label = document.createElement('p');
label.classList.add('toolbar-label');
label.innerText = "Update ReadCube";
var icon = document.createElement('i');
icon.classList.add('fa','fa-cube','fa-fw');
//icon.innerText = "::before";
child.appendChild(icon);
child.appendChild(label);
sect.appendChild(child);
var second = parent.getElementsByTagName('div')[1]
parent.insertBefore(sect, second);
parent.setAttribute('readcube', 'injected');
}, 1000);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment