/** * HTML link-extraction helpers. * * The browser-side extraction lives inline in `crawler.extractLinks` so it * can run via `page.evaluate`. This module holds the bits that are either * pure (serializable without a DOM) or shared with other extractors. */ /** * Parse a `` content attribute and * return the redirect URL, or null if the value doesn't carry one. * * Accepts any of these real-world shapes: * "0;url=/next" * "0; URL=/next" * "5; url='/next'" * "3;URL=https://example.com/" * * A plain `` (delay only, no URL) * returns null. */ export function parseMetaRefreshUrl(content: string | null | undefined): string | null { if (!content) return null; // The first segment before the semicolon is the delay. Anything after is // parameter=value pairs, though in practice only `url=` is used. const semi = content.indexOf(";"); if (semi === -1) return null; const rest = content.slice(semi + 1).trim(); const m = rest.match(/^url\s*=\s*(.*)$/i); if (!m) return null; let url = m[1]!.trim(); if (url.length === 0) return null; // Quoted URLs delimit with the matching quote — the URL may legitimately // contain `;`, so we can't just split on it. Unterminated quotes are a // malformed directive; return null rather than a truncated URL. if (url.startsWith('"') || url.startsWith("'")) { const quote = url[0]!; const end = url.indexOf(quote, 1); if (end === -1) return null; url = url.slice(1, end); } else { // Unquoted: terminate at the next parameter separator. Without this, // `0;url=/next;foo=bar` would captured as `/next;foo=bar` and queue // the wrong URL. const sep = url.indexOf(";"); if (sep !== -1) url = url.slice(0, sep).trim(); } return url.length > 0 ? url : null; }