Skip to content

Instantly share code, notes, and snippets.

@PaulVanSchayck
Forked from tillahoffmann/readleaf.user.js
Last active October 6, 2023 21:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PaulVanSchayck/3e61439f55ea425dcdb9477dfa329c86 to your computer and use it in GitHub Desktop.
Save PaulVanSchayck/3e61439f55ea425dcdb9477dfa329c86 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.4
// @description Adds an "Update Library" button to Overleaf that allows you to import your Readcube library.
// @author Till Hoffmann, Michal Ptacek, Paul van Schayck
// @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;
}
function formatType(x) {
const types = {
'conference_paper' : 'inproceedings',
'poster': 'inproceedings',
'webpage': 'misc',
'report': 'techreport'
}
if( x in types ) {
return types[x]
} else {
return x
}
}
function formatRaw(x) {
return x;
}
const formattingLookup = {
'title': 'article/title',
'journal': 'article/journal',
'pages': {
'path': 'article/pagination',
'format': formatPageNumbers,
},
'volume': 'article/volume',
'year': 'article/year',
'doi': {
'path': 'ext_ids/doi',
'format': formatRaw
},
'url': {
'path': 'custom_metadata/url',
'format': formatRaw
},
'publisher': 'custom_metadata/publisher'
};
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 = [
'@' + formatType(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 parseBibTex(bibtex) {
const entries = bibtex.split('\n\n');
const link = entries.shift();
const parsedEntries = entries.map(entry => {
const lines = entry.split('\n');
const type = lines[0].split('{')[0].substring(1);
const citationKey = lines[0].split('{')[1].split(',')[0];
const fields = {};
for (let i = 1; i < lines.length - 1; i++) {
const key = lines[i].split('=')[0].trim();
let value = lines[i].split('=')[1].trim();
value = value.substring(1, value.length - 2);
fields[key] = value;
}
return {type, citationKey, fields};
});
return [link, parsedEntries];
}
function sortBibTex(bibtex) {
const parsed= parseBibTex(bibtex);
const link = parsed[0];
const parsedEntries = parsed[1]
parsedEntries.sort((a, b) => {
const authorA = a.fields.author.split('and')[0].trim().split(' ').pop();
const authorB = b.fields.author.split('and')[0].trim().split(' ').pop();
return authorA.localeCompare(authorB);
});
let sortedBibTex = '';
for (const entry of parsedEntries) {
sortedBibTex += `@${entry.type}{${entry.citationKey},\n`;
for (const [key, value] of Object.entries(entry.fields)) {
sortedBibTex += ` ${key} = {${value}},\n`;
}
sortedBibTex += '}\n\n';
}
return link + '\n\n' + sortedBibTex;
}
function scrollToTop() {
var scroller = document.querySelector('div.cm-scroller');
scroller.scrollTop = 0;
}
function updateLibrary() {
// Get the first editor and fetch the code
//for (let element of document.getElementsByClassName('editor')) {
//editor = ace.edit(element);
//editor = element
//break;
//}
var editor = document.getElementsByClassName('cm-content')[0];
//var code = editor.getValue();
var code = editor.innerText;
var chbox = document.getElementById('check1');
// 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'));
if (chbox.checked) {
editor.innerText = sortBibTex(lines.join('\n\n'))
} else {
editor.innerText = 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','layout-dropdown','dropdown','btn-group');
// Creating button itself
var button = document.createElement('button');
button.classList.add('btn', 'btn-info', 'btn-full-height');
button.onclick = function() {
scrollToTop();
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";
button.appendChild(icon);
button.appendChild(label);
sect.appendChild(button);
//
//var dropdownMenu = document.createElement('div')
// create the dropdown menu (list)
var dropdown = document.createElement('ul');
dropdown.classList.add('layout-dropdown-list','dropdown-menu','dropdown-menu-right');
// create first item
var firstItem = document.createElement('li');
dropdown.appendChild(firstItem);
var firstItemA = document.createElement('a');
firstItem.appendChild(firstItemA);
var firstItemD = document.createElement('div');
firstItemD.classList.add('layout-menu-item');
firstItemA.appendChild(firstItemD);
var firstItemDiv = document.createElement('div');
firstItemDiv.classList.add('layout-menu-item-start');
firstItemD.appendChild(firstItemDiv);
// Create the sort checkbox
var sortLabel = document.createElement('div');
sortLabel.innerText = 'Sort alphabetically';
var sortIcon = document.createElement('i');
sortIcon.classList.add('fa','fa-fw','fa-sort');
var sortCheckbox = document.createElement('input');
sortCheckbox.type = "checkbox";
sortCheckbox.setAttribute('id','check1');
// Default value for checkbox - sort alphabetically
sortCheckbox.checked = true;
firstItemDiv.appendChild(sortCheckbox);
firstItemDiv.appendChild(sortIcon);
firstItemDiv.appendChild(sortLabel);
// Add the drop-down menu to the sect element
sect.appendChild(dropdown);
// Show the drop-down menu when the user hovers over the button
button.onmouseover = function() {
dropdown.style.display = "block";
}
button.onmouseout = function() {
dropdown.style.display = "none";
}
// timeout:
var timeout;
button.onmouseover = function() {
timeout = setTimeout(function() {
dropdown.style.display = "block";
}, 1500);
}
button.onmouseout = function() {
clearTimeout(timeout);
}
document.addEventListener("click", function(event) {
if (!dropdown.contains(event.target)) {
dropdown.style.display = "none";
}
});
//
var second = parent.getElementsByTagName('div')[1]
parent.insertBefore(sect, second);
parent.setAttribute('readcube', 'injected');
}, 1500);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment