export type PdfRect = [number, number, number, number]; export interface ParsedPdfAnchor { page: number; rects: PdfRect[]; } const PDF_ANCHOR_PREFIX = "pdf:v1:"; export function parsePageHref(href: string): number | null { const match = href.match(/^pdf:page:(\d+)$/); if (!match) return null; const page = Number(match[1]); return Number.isFinite(page) && page > 0 ? page : null; } export function serializePdfAnchor(page: number, rects?: PdfRect[]): string { const base = `${PDF_ANCHOR_PREFIX}p=${page}`; if (!rects?.length) return base; return `${base};rects=${encodeURIComponent(JSON.stringify(rects))}`; } export function parsePdfAnchor(anchor: string): ParsedPdfAnchor | null { if (anchor.startsWith("pdf:page:")) { const page = parsePageHref(anchor); return page ? { page, rects: [] } : null; } if (!anchor.startsWith(PDF_ANCHOR_PREFIX)) return null; const pageMatch = anchor.match(/p=(\d+)/); if (!pageMatch) return null; const page = Number(pageMatch[1]); if (!Number.isFinite(page) || page < 1) return null; const rectsMatch = anchor.match(/rects=([^;]+)/); let rects: PdfRect[] = []; if (rectsMatch?.[1]) { try { const parsed = JSON.parse(decodeURIComponent(rectsMatch[1])) as unknown; if (Array.isArray(parsed)) { rects = parsed.filter( (r): r is PdfRect => Array.isArray(r) && r.length === 4 && r.every((n) => typeof n === "number" && Number.isFinite(n)), ); } } catch { rects = []; } } return { page, rects }; }