import { decodeEntities } from "../html.js"; import { type FetchContext, type HandlerResult, FetchError, defaultFetch, defineHandler, getText } from "./handler.js"; /** * "/pdf/2301.00001v2.pdf" -> "2301.00001". * Both suffixes can be present, so they come off one at a time; old-style ids keep their archive prefix ("math/0309136"). */ export function paperId(pathname: string): string { return paperPdfId(pathname).replace(/v\d+$/, ""); } /** PDF endpoints are versioned; unlike the metadata API, keep an explicit vN. */ export function paperPdfId(pathname: string): string { return pathname.replace(/^\/(abs|pdf|html)\//, "").replace(/\.pdf$/, ""); } async function fromExportApi(id: string, ctx: FetchContext): Promise { const { text: xml } = await getText(`https://export.arxiv.org/api/query?id_list=${id}`, ctx.signal); const entry = xml.split("")[1]; if (!entry) throw new FetchError(`arXiv paper ${id} not found`); const pick = (tag: string) => decodeEntities((entry.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)`))?.[1] ?? "").trim()); const authors = [...entry.matchAll(/([^<]+)<\/name>/g)].map((m) => m[1]); const title = pick("title").replace(/\s+/g, " "); const parts = [ `# ${title}`, `arXiv:${id} | ${authors.join(", ")} | published ${pick("published").slice(0, 10)}`, `pdf: https://arxiv.org/pdf/${id}`, "", "## Abstract", pick("summary").replace(/\s*\n\s*/g, " "), ]; return { kind: "paper", title, content: parts.join("\n") }; } export const arxivHandler = defineHandler({ name: "arxiv", description: "arXiv papers: title, authors, abstract via the export API; full text for /pdf/ urls and full mode", match: (url) => /(^|\.)arxiv\.org$/.test(url.hostname) && /^\/(abs|pdf|html)\//.test(url.pathname), async fetch(url, ctx) { const id = paperId(url.pathname); // Asking for /pdf/ (or full mode) means the caller wants the paper, not a summary of it. // Metadata is a page of text; the paper is the reason the url was passed at all. // defaultFetch sees application/pdf and extracts. if (ctx.mode === "full" || /^\/pdf\//.test(url.pathname)) { const paper = await defaultFetch(new URL(`https://arxiv.org/pdf/${paperPdfId(url.pathname)}`), { ...ctx, mode: "light" }); return { ...paper, kind: "paper" }; } try { return await fromExportApi(id, ctx); } catch { // export.arxiv.org rate-limits hard and drops out for stretches, while arxiv.org itself stays up. // The abstract page carries the same metadata, so a failed api call is worth a second try rather than an error the model has to work around. return defaultFetch(new URL(`https://arxiv.org/abs/${id}`), { ...ctx, mode: "light" }); } }, });