Add buttons to jump between arxiv.org and alphaxiv.org for the same paper
// ==UserScript==
// @name ArXiv-AlphaXiv Navigator
// @namespace https://github.com/pangahn/arxiv-navigator
// @version 0.1
// @description Add buttons to jump between arxiv.org and alphaxiv.org for the same paper
// @author pangahn
// @match https://*.arxiv.org/abs/*
// @match https://www.alphaxiv.org/abs/*
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Debug utility function
function log(message) {
const DEBUG = true; // Set to false to disable logs in production
if (DEBUG) console.log(`[ArXiv-AlphaXiv Navigator] ${message}`);
}
// Determine which site we're on
const isArXiv = window.location.hostname.includes('arxiv.org');
const isAlphaXiv = window.location.hostname.includes('alphaxiv.org');
// Function to get the paper ID from the URL
function getPaperId() {
try {
const url = window.location.href;
let match;
if (isArXiv) {
// Match both formats: 2504.04736 and 2504.04736v2
match = url.match(/arxiv\.org\/abs\/(\d+\.\d+(?:v\d+)?)/);
} else if (isAlphaXiv) {
match = url.match(/alphaxiv\.org\/abs\/(.+)$/);
}
if (!match) {
log(`Failed to extract paper ID from URL: ${url}`);
return null;
}
return match[1];
} catch (error) {
log(`Error extracting paper ID: ${error.message}`);
return null;
}
}
// Function to create and add jump button on ArXiv pages
function addAlphaXivButton() {
try {
// Check if button already exists
if (document.querySelector('a[href^="https://www.alphaxiv.org/abs/"]')) {
log('AlphaXiv button already exists');
return true;
}
const paperId = getPaperId();
if (!paperId) {
log('No paper ID found, cannot add AlphaXiv button');
return false;
}
// Extract the base paper ID without version if present
const baseId = paperId.split('v')[0];
// Create the button element with consistent styling
const button = document.createElement('a');
button.href = `https://www.alphaxiv.org/abs/${baseId}`;
button.className = 'abs-button download-pdf alphaxiv-jump-button';
button.target = '_blank';
button.textContent = 'View on AlphaXiv';
button.style.display = 'inline-block';
button.setAttribute('data-arxiv-navigator', 'true');
button.setAttribute('title', 'Open this paper on AlphaXiv');
// Find the View PDF link
const pdfLink = document.querySelector('a.download-pdf');
if (pdfLink) {
// Find the parent li element
const parentLi = pdfLink.closest('li');
if (parentLi) {
// Create a new li element for our button
const newLi = document.createElement('li');
newLi.appendChild(button);
// Insert after the PDF link's li element
if (parentLi.nextSibling) {
parentLi.parentNode.insertBefore(newLi, parentLi.nextSibling);
} else {
parentLi.parentNode.appendChild(newLi);
}
log('AlphaXiv button added successfully');
return true;
}
}
return false;
} catch (error) {
log(`Error adding AlphaXiv button: ${error.message}`);
return false;
}
}
// Function to create and add jump button on AlphaXiv pages
function addArXivButton() {
try {
// Check if button already exists - faster than checking DOM structure
if (document.querySelector('.arxiv-jump-button')) {
log('ArXiv jump button already exists');
return true;
}
// Use more precise selectors to find the target div container
// Prioritize the most specific selector, then use backup selectors
let targetDiv = document.querySelector('[data-sentry-component="RightSection"] .flex.items-center.space-x-2');
// Backup selector 1: Locate through div containing the download button
if (!targetDiv) {
const downloadButton = document.querySelector('button[aria-label="Download from arXiv"]');
if (downloadButton) {
targetDiv = downloadButton.closest('.flex.items-center.space-x-2');
}
}
// Backup selector 2: Locate through button containing thumbs-up
if (!targetDiv) {
const thumbsUpButton = document.querySelector('svg.lucide-thumbs-up');
if (thumbsUpButton) {
targetDiv = thumbsUpButton.closest('.flex.items-center.space-x-2');
}
}
// Backup selector 3: Locate through div containing bookmark
if (!targetDiv) {
const bookmarkDiv = document.querySelector('[data-sentry-component="PaperFeedBookmarks"]');
if (bookmarkDiv) {
targetDiv = bookmarkDiv.closest('.flex.items-center.space-x-2');
}
}
// Backup selector 4: Last generic selector
if (!targetDiv) {
targetDiv = document.querySelector('.flex.items-center.space-x-2');
}
if (!targetDiv) {
log('Target div not found, retrying...');
return false;
}
// Double check if button has already been added
if (targetDiv.querySelector('.arxiv-jump-button')) {
return true;
}
// Get paper ID from current URL
const paperId = getPaperId();
if (!paperId) {
log('Paper ID not found in URL');
return true;
}
const arxivUrl = `https://www.arxiv.org/abs/${paperId}`;
// Create ArXiv jump button
const arxivButton = document.createElement('button');
arxivButton.className = 'arxiv-jump-button inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm ring-offset-white transition-all duration-200 outline-hidden focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-neutral-950 hover:bg-[#9a20360a] hover:text-custom-red dark:text-white dark:hover:bg-custom-red/25 enabled:active:ring-2 enabled:active:ring-[#9a20360a] size-10 rounded-full! h-8 w-8';
arxivButton.setAttribute('aria-label', 'Jump to ArXiv');
arxivButton.setAttribute('title', 'Jump to ArXiv');
arxivButton.setAttribute('data-arxiv-navigator', 'true');
// Add ArXiv icon (using external link icon)
arxivButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link size-4" aria-hidden="true">
<path d="M15 3h6v6"></path>
<path d="M10 14 21 3"></path>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
</svg>
`;
// Add click event
arxivButton.addEventListener('click', function(e) {
e.preventDefault();
window.open(arxivUrl, '_blank');
});
// Insert button at the first position in div
targetDiv.insertBefore(arxivButton, targetDiv.firstChild);
log('ArXiv jump button added successfully');
return true;
} catch (error) {
log(`Error adding ArXiv button: ${error.message}`);
return false;
}
}
// Main initialization function
function initNavigator() {
let success = false;
if (isArXiv) {
success = addAlphaXivButton();
} else if (isAlphaXiv) {
success = addArXivButton();
}
return success;
}
// Maintain state of button addition attempts
let buttonAddAttempts = 0;
const MAX_ATTEMPTS = 10;
// Rate limiter for observer callback to prevent excessive processing
let lastProcessTime = 0;
const THROTTLE_INTERVAL = 200; // ms
// Function to handle initialization with retry logic
function tryAddButton() {
// Check if button already exists
if ((isArXiv && document.querySelector('.alphaxiv-jump-button')) ||
(isAlphaXiv && document.querySelector('.arxiv-jump-button'))) {
log('Button already exists, no need to add');
return true;
}
// Try to add the button
const success = initNavigator();
// Increment attempt counter
buttonAddAttempts++;
if (success) {
log(`Successfully added button on attempt ${buttonAddAttempts}`);
return true;
} else if (buttonAddAttempts >= MAX_ATTEMPTS) {
log(`Failed to add button after ${MAX_ATTEMPTS} attempts, giving up`);
return true; // Return true to stop retrying
}
return false;
}
// Throttled mutation observer callback
function throttledObserverCallback(mutations) {
const now = Date.now();
if (now - lastProcessTime < THROTTLE_INTERVAL) return;
lastProcessTime = now;
// Check if button already exists
if ((isArXiv && document.querySelector('.alphaxiv-jump-button')) ||
(isAlphaXiv && document.querySelector('.arxiv-jump-button'))) {
observer.disconnect();
return;
}
// Process mutations
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Delay execution to ensure DOM is fully updated
setTimeout(function() {
if (tryAddButton()) {
observer.disconnect();
}
}, 100);
break;
}
}
}
// Create MutationObserver
const observer = new MutationObserver(throttledObserverCallback);
// First attempt after DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
if (!tryAddButton()) {
// Start observing DOM changes if button wasn't added
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}, 300);
});
} else {
// If page is already loaded, try immediately
setTimeout(function() {
if (!tryAddButton()) {
// Start observing DOM changes if button wasn't added
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}, 300);
}
// Stop observing after 10 seconds (to prevent infinite observation)
setTimeout(function() {
if (observer) {
observer.disconnect();
log('Disconnected observer after timeout');
}
}, 10000);
})();