// author: simon-pierre boucher import { err, ok, type Result } from "./result.js"; import { tendrilError } from "./errors.js"; const DEFAULT_PORTS: Readonly> = { "http:": "80", "https:": "443", }; const TRACKING_PARAMS = new Set(["fbclid", "gclid", "gclsrc", "ref", "mc_cid", "mc_eid", "_ga", "_gl"]); function isTrackingParam(key: string): boolean { const lower = key.toLowerCase(); return lower.startsWith("utm_") || TRACKING_PARAMS.has(lower); } /** * Canonicalize a URL for dedup and cache keys (§10 rule 1, §10.1). The result is * intended for comparison only — keep the original string for output. Returns * ERR_INVALID_URL for anything that is not an absolute http(s) URL. */ export function normalizeUrl(raw: string, base?: string): Result { let u: URL; try { u = base !== undefined ? new URL(raw, base) : new URL(raw); } catch { return err(tendrilError("ERR_INVALID_URL", { details: { url: raw } })); } if (u.protocol !== "http:" && u.protocol !== "https:") { return err(tendrilError("ERR_INVALID_URL", { details: { url: raw, scheme: u.protocol } })); } u.hostname = u.hostname.toLowerCase(); if (u.port !== "" && DEFAULT_PORTS[u.protocol] === u.port) { u.port = ""; } const kept: Array<[string, string]> = []; for (const [key, value] of u.searchParams.entries()) { if (!isTrackingParam(key)) kept.push([key, value]); } kept.sort((a, b) => (a[0] === b[0] ? (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) : a[0] < b[0] ? -1 : 1)); u.search = ""; for (const [key, value] of kept) u.searchParams.append(key, value); const hashbang = u.hash.startsWith("#!"); if (!hashbang) u.hash = ""; if (u.pathname.length > 1 && u.pathname.endsWith("/")) { u.pathname = u.pathname.replace(/\/+$/, ""); if (u.pathname === "") u.pathname = "/"; } return ok(u.toString()); } export interface ParsedUrl { readonly url: URL; readonly host: string; } export function parseHost(raw: string): Result { try { const url = new URL(raw); return ok({ url, host: url.hostname.toLowerCase() }); } catch { return err(tendrilError("ERR_INVALID_URL", { details: { url: raw } })); } }