// ==UserScript== // @name arXiv AlphaXiv 跳转器 // @namespace https://greasyfork.org/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 // @downloadURL https://update.greasyfork.icu/scripts/576786/arXiv%20AlphaXiv%20%E8%B7%B3%E8%BD%AC%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/576786/arXiv%20AlphaXiv%20%E8%B7%B3%E8%BD%AC%E5%99%A8.meta.js // ==/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 = `