Greasy Fork is available in English.
按论文 ID 在 arXiv 摘要、HTML、PDF 和 AlphaXiv 页面之间跳转。
// ==UserScript==
// @name arXiv AlphaXiv 跳转器
// @namespace http://greasyfork.icu/zh-CN/scripts/576786-arxiv-alphaxiv-%E8%B7%B3%E8%BD%AC%E5%99%A8
// @version 1.0.1
// @description 按论文 ID 在 arXiv 摘要、HTML、PDF 和 AlphaXiv 页面之间跳转。
// @match https://arxiv.org/abs/*
// @match https://arxiv.org/html/*
// @match https://arxiv.org/pdf/*
// @match https://www.arxiv.org/abs/*
// @match https://www.arxiv.org/html/*
// @match https://www.arxiv.org/pdf/*
// @match https://alphaxiv.org/abs/*
// @match https://www.alphaxiv.org/abs/*
// @match https://alphaxiv.org/overview/*
// @match https://www.alphaxiv.org/overview/*
// @run-at document-idle
// @connect arxiv.org
// @connect www.arxiv.org
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const POSITION_STORAGE_KEY = 'arxiv-alphaxiv-jumper-position';
const MODERN_ID_RE = /\b\d{4}\.\d{4,5}(?:v\d+)?\b/i;
const LEGACY_ID_RE = /\b(?:astro-ph|cond-mat|gr-qc|hep-ex|hep-lat|hep-ph|hep-th|math(?:\.[A-Z]{2})?|math-ph|nlin(?:\.[A-Z]{2})?|nucl-ex|nucl-th|physics(?:\.[a-z-]+)?|quant-ph|cs(?:\.[A-Z]{2})?|q-bio(?:\.[A-Z]{2})?|q-fin(?:\.[A-Z]{2})?|stat(?:\.[A-Z]{2})?)\/\d{7}(?:v\d+)?\b/i;
function normalizeArxivId(value) {
if (!value) return '';
let text = decodeURIComponent(String(value)).trim();
text = text.replace(/^https?:\/\/(?:www\.)?(?:arxiv\.org|alphaxiv\.org)\//i, '');
text = text.replace(/^(?:abs|html|pdf)\//i, '');
text = text.replace(/[?#].*$/, '');
text = text.replace(/\.pdf$/i, '');
text = text.replace(/\/+$/, '');
const modernId = text.match(MODERN_ID_RE);
if (modernId) return modernId[0];
const legacyId = text.match(LEGACY_ID_RE);
if (legacyId) return legacyId[0];
return '';
}
function getAttributeId(selector, attribute) {
const node = document.querySelector(selector);
return node ? normalizeArxivId(node.getAttribute(attribute)) : '';
}
function extractArxivId() {
const pathId = normalizeArxivId(window.location.pathname);
if (pathId) return pathId;
const canonicalId = getAttributeId('link[rel="canonical"]', 'href');
if (canonicalId) return canonicalId;
const ogUrlId = getAttributeId('meta[property="og:url"]', 'content');
if (ogUrlId) return ogUrlId;
const link = document.querySelector('a[href*="arxiv.org/abs/"], a[href*="arxiv.org/html/"], a[href*="arxiv.org/pdf/"], a[href*="alphaxiv.org/abs/"]');
const linkId = link ? normalizeArxivId(link.href) : '';
if (linkId) return linkId;
return normalizeArxivId(document.body ? document.body.innerText : '');
}
function stripVersion(id) {
return id.replace(/v\d+$/i, '');
}
function encodeArxivId(id) {
return id.split('/').map(encodeURIComponent).join('/');
}
function getCurrentKind() {
const host = window.location.hostname.replace(/^www\./i, '').toLowerCase();
const path = window.location.pathname;
if (host === 'alphaxiv.org' && /^\/abs\//i.test(path)) return 'alphaabs';
if (host === 'alphaxiv.org' && /^\/overview\//i.test(path)) return 'alphablog';
if (host !== 'arxiv.org') return '';
if (/^\/abs\//i.test(path)) return 'abs';
if (/^\/html\//i.test(path)) return 'html';
if (/^\/pdf\//i.test(path)) return 'pdf';
return '';
}
function requestText(url) {
if (typeof GM_xmlhttpRequest === 'function') {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url,
onload: (response) => resolve(response.responseText || ''),
onerror: () => resolve(''),
ontimeout: () => resolve('')
});
});
}
return fetch(url).then((response) => response.ok ? response.text() : '').catch(() => '');
}
function extractGithubRepo(text) {
const skippedOwners = new Set(['collections', 'events', 'explore', 'features', 'login', 'marketplace', 'new', 'orgs', 'pricing', 'search', 'settings', 'topics', 'trending', 'users']);
const matches = String(text || '').matchAll(/https?:\/\/(?:www\.)?github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)/gi);
for (const match of matches) {
const owner = match[1];
const repo = match[2].replace(/(?:\.git)?[).,;:'"]*$/i, '');
if (repo && !skippedOwners.has(owner.toLowerCase())) return `https://github.com/${owner}/${repo}`;
}
return '';
}
async function findGithubRepo(id) {
if (getCurrentKind() === 'abs') {
const currentRepo = extractGithubRepo(document.documentElement.innerHTML);
if (currentRepo) return currentRepo;
}
const absHtml = await requestText(`https://arxiv.org/abs/${encodeArxivId(id)}`);
return extractGithubRepo(absHtml);
}
function buildTargets(id, githubRepo) {
const encodedId = encodeArxivId(id);
const encodedVersionlessId = encodeArxivId(stripVersion(id));
const targets = [
{ key: 'abs', label: 'Abs', url: `https://arxiv.org/abs/${encodedId}` },
{ key: 'html', label: 'Html', url: `https://arxiv.org/html/${encodedId}` },
{ key: 'pdf', label: 'pdf', url: `https://arxiv.org/pdf/${encodedId}` },
{ key: 'alphaabs', label: 'αAbs', url: `https://www.alphaxiv.org/abs/${encodedVersionlessId}` },
{ key: 'alphablog', label: 'αBlog', url: `https://www.alphaxiv.org/overview/${encodedVersionlessId}` }
];
if (githubRepo) targets.push({ key: 'github', label: 'git', url: githubRepo });
return targets;
}
function createPanel(id, githubRepo) {
const currentKind = getCurrentKind();
const targets = buildTargets(id, githubRepo);
const currentTarget = targets.find((target) => target.key === currentKind);
const visibleTargets = currentTarget ? targets.filter((target) => target.key !== currentKind) : targets;
const root = document.createElement('div');
const shadow = root.attachShadow({ mode: 'open' });
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
let expanded = false;
let drag = null;
let dragged = false;
root.id = 'arxiv-alphaxiv-jumper-root';
root.style.cssText = 'position:fixed;top:64px;right:8px;z-index:2147483647;';
try {
const position = JSON.parse(window.localStorage.getItem(POSITION_STORAGE_KEY));
if (position && Number.isFinite(position.left) && Number.isFinite(position.top)) {
root.style.left = `${clamp(position.left, 0, Math.max(0, window.innerWidth - 40))}px`;
root.style.top = `${clamp(position.top, 0, Math.max(0, window.innerHeight - 28))}px`;
root.style.right = 'auto';
}
} catch (error) {
}
shadow.innerHTML = `
<style>
:host { all: initial; }
.panel {
width: 64px;
overflow: hidden;
border: 1px solid rgba(17,24,39,.14);
border-radius: 8px;
background: rgba(255,255,255,.96);
box-shadow: 0 6px 14px rgba(17,24,39,.15);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button {
width: 100%;
border: 0;
padding: 5px 0;
color: #fff;
background: #2563eb;
font: 600 12px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
cursor: move;
user-select: none;
touch-action: none;
}
#list {
display: grid;
gap: 3px;
padding: 4px;
}
#list[hidden] { display: none; }
a {
padding: 4px 0;
border-radius: 5px;
color: #1f2937;
background: #f3f4f6;
font: 500 11px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
text-align: center;
text-decoration: none;
}
a:hover { color: #fff; background: #2563eb; }
</style>
<div class="panel">
<button id="toggle" type="button" aria-expanded="${expanded}" title="拖动或展开">${currentTarget ? currentTarget.label : '跳转'}</button>
<div id="list"${expanded ? '' : ' hidden'}>
${visibleTargets.map((target) => `<a href="${target.url}">${target.label}</a>`).join('')}
</div>
</div>
`;
const toggle = shadow.getElementById('toggle');
const list = shadow.getElementById('list');
toggle.addEventListener('pointerdown', (event) => {
if (event.button !== 0) return;
const rect = root.getBoundingClientRect();
drag = {
id: event.pointerId,
x: event.clientX,
y: event.clientY,
left: rect.left,
top: rect.top,
moved: false
};
root.style.left = `${rect.left}px`;
root.style.top = `${rect.top}px`;
root.style.right = 'auto';
toggle.setPointerCapture(event.pointerId);
});
toggle.addEventListener('pointermove', (event) => {
if (!drag || event.pointerId !== drag.id) return;
const dx = event.clientX - drag.x;
const dy = event.clientY - drag.y;
drag.moved = drag.moved || Math.abs(dx) > 3 || Math.abs(dy) > 3;
if (!drag.moved) return;
const rect = root.getBoundingClientRect();
root.style.left = `${clamp(drag.left + dx, 0, Math.max(0, window.innerWidth - rect.width))}px`;
root.style.top = `${clamp(drag.top + dy, 0, Math.max(0, window.innerHeight - rect.height))}px`;
event.preventDefault();
});
function stopDrag(event) {
if (!drag || event.pointerId !== drag.id) return;
dragged = drag.moved;
if (dragged) {
const rect = root.getBoundingClientRect();
window.localStorage.setItem(POSITION_STORAGE_KEY, JSON.stringify({
left: Math.round(rect.left),
top: Math.round(rect.top)
}));
}
drag = null;
}
toggle.addEventListener('pointerup', stopDrag);
toggle.addEventListener('pointercancel', stopDrag);
toggle.addEventListener('click', () => {
if (dragged) {
dragged = false;
return;
}
expanded = !expanded;
list.hidden = !expanded;
toggle.setAttribute('aria-expanded', String(expanded));
});
document.documentElement.appendChild(root);
}
async function init() {
const id = extractArxivId();
if (!id || document.getElementById('arxiv-alphaxiv-jumper-root')) return;
createPanel(id, await findGithubRepo(id));
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();