Greasy Fork

Greasy Fork is available in English.

arXiv AlphaXiv 跳转器

按论文 ID 在 arXiv 摘要、HTML、PDF 和 AlphaXiv 页面之间跳转。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
  }
})();