UNPKG

@wevu/web-apis

Version:

Web API polyfills and global installers for mini-program runtimes

210 lines (209 loc) 6.58 kB
//#region src/url.ts const PLUS_REGEXP = /\+/g; const LEADING_QUERY_REGEXP = /^\?/; const ABSOLUTE_URL_REGEXP = /^([a-z][a-z\d+.-]*:)?\/\/([^/?#]+)(\/[^?#]*)?(\?[^#]*)?(#.*)?$/i; const ABSOLUTE_URL_PREFIX_REGEXP = /^[a-z][a-z\d+.-]*:\/\//i; const ENCODED_SPACE_REGEXP = /%20/g; const HOST_WITH_PORT_REGEXP = /^([^:]*)(?::(.*))?$/; function encodeSearchParam(value) { return encodeURIComponent(value).replace(ENCODED_SPACE_REGEXP, "+"); } function decodeSearchParam(value) { return decodeURIComponent(value.replace(PLUS_REGEXP, " ")); } function normalizeSearchSource(input) { return input.replace(LEADING_QUERY_REGEXP, ""); } function parseSearchEntries(input) { if (!input) return []; return normalizeSearchSource(input).split("&").filter(Boolean).map((segment) => { const separatorIndex = segment.indexOf("="); if (separatorIndex === -1) return [decodeSearchParam(segment), ""]; return [decodeSearchParam(segment.slice(0, separatorIndex)), decodeSearchParam(segment.slice(separatorIndex + 1))]; }); } function serializeSearchEntries(entries) { return entries.map(([key, value]) => `${encodeSearchParam(key)}=${encodeSearchParam(value)}`).join("&"); } function parseAbsoluteUrl(input) { const match = input.match(ABSOLUTE_URL_REGEXP); if (!match) return null; const protocol = match[1] ?? ""; const host = match[2] ?? ""; const pathname = match[3] || "/"; const search = match[4] ?? ""; const hash = match[5] ?? ""; const hostnameMatch = host.match(HOST_WITH_PORT_REGEXP); return { protocol, host, hostname: hostnameMatch?.[1] ?? host, port: hostnameMatch?.[2] ?? "", pathname, search, hash, origin: protocol && host ? `${protocol}//${host}` : "", href: `${protocol}//${host}${pathname}${search}${hash}` }; } function resolveRelativeUrl(input, base) { const parsedBase = parseAbsoluteUrl(base); if (!parsedBase) throw new TypeError(`Failed to construct URL from base ${base}`); if (input.startsWith("//")) return `${parsedBase.protocol}${input}`; if (input.startsWith("/")) return `${parsedBase.origin}${input}`; if (input.startsWith("?") || input.startsWith("#")) { const pathname = parsedBase.pathname || "/"; return `${`${parsedBase.origin}${pathname}`}${input}`; } const basePathSegments = parsedBase.pathname.split("/").slice(0, -1); for (const segment of input.split("/")) { if (!segment || segment === ".") continue; if (segment === "..") { basePathSegments.pop(); continue; } basePathSegments.push(segment); } return `${parsedBase.origin}/${basePathSegments.join("/")}`; } var URLSearchParamsPolyfill = class { entriesStore = []; constructor(init, onChange) { this.onChange = onChange; if (!init) return; if (typeof init === "string") { this.entriesStore.push(...parseSearchEntries(init)); return; } if (typeof init[Symbol.iterator] === "function") { for (const [key, value] of init) this.append(key, value); return; } for (const [key, value] of Object.entries(init)) { if (Array.isArray(value)) { for (const item of value) this.append(key, item); continue; } this.append(key, value); } } append(key, value) { this.entriesStore.push([String(key), String(value)]); this.onChange?.(); } delete(key) { const normalizedKey = String(key); let changed = false; for (let i = this.entriesStore.length - 1; i >= 0; i--) if (this.entriesStore[i]?.[0] === normalizedKey) { this.entriesStore.splice(i, 1); changed = true; } if (changed) this.onChange?.(); } get(key) { const normalizedKey = String(key); return this.entriesStore.find(([entryKey]) => entryKey === normalizedKey)?.[1] ?? null; } getAll(key) { const normalizedKey = String(key); return this.entriesStore.filter(([entryKey]) => entryKey === normalizedKey).map(([, value]) => value); } has(key) { return this.entriesStore.some(([entryKey]) => entryKey === String(key)); } set(key, value) { this.delete(key); this.append(key, value); } forEach(callback) { for (const [key, value] of this.entriesStore) callback(value, key); } entries() { return this.entriesStore[Symbol.iterator](); } keys() { return this.entriesStore.map(([key]) => key)[Symbol.iterator](); } values() { return this.entriesStore.map(([, value]) => value)[Symbol.iterator](); } toString() { return serializeSearchEntries(this.entriesStore); } [Symbol.iterator]() { return this.entries(); } }; var URLPolyfill = class { hashValue = ""; hrefValue = ""; searchValue = ""; host = ""; hostname = ""; origin = ""; password = ""; pathname = "/"; port = ""; protocol = ""; username = ""; searchParams; constructor(input, base) { const inputString = typeof input === "string" ? input : input.toString(); const baseString = typeof base === "string" ? base : base ? base.toString() : void 0; const parsed = parseAbsoluteUrl(ABSOLUTE_URL_PREFIX_REGEXP.test(inputString) ? inputString : baseString ? resolveRelativeUrl(inputString, baseString) : inputString); if (!parsed) throw new TypeError(`Failed to construct URL from ${inputString}`); this.protocol = parsed.protocol; this.host = parsed.host; this.hostname = parsed.hostname; this.port = parsed.port; this.pathname = parsed.pathname; this.searchValue = parsed.search; this.hashValue = parsed.hash; this.origin = parsed.origin; this.searchParams = new URLSearchParamsPolyfill(parsed.search, () => { this.syncSearchFromParams(); }); this.updateHref(); } get hash() { return this.hashValue; } set hash(value) { this.hashValue = value ? value.startsWith("#") ? value : `#${value}` : ""; this.updateHref(); } get href() { return this.hrefValue; } get search() { return this.searchValue; } set search(value) { this.searchValue = value ? value.startsWith("?") ? value : `?${value}` : ""; this.resetSearchParams(this.searchValue); this.updateHref(); } toString() { return this.hrefValue; } toJSON() { return this.toString(); } resetSearchParams(value) { const nextParams = new URLSearchParamsPolyfill(value); this.searchParams.entriesStore.splice(0, this.searchParams.entriesStore.length); nextParams.forEach((entryValue, entryKey) => { this.searchParams.entriesStore.push([entryKey, entryValue]); }); } updateHref() { this.hrefValue = `${this.protocol}//${this.host}${this.pathname}${this.searchValue}${this.hashValue}`; } syncSearchFromParams() { const search = this.searchParams.toString(); this.searchValue = search ? `?${search}` : ""; this.updateHref(); } }; //#endregion export { URLPolyfill, URLSearchParamsPolyfill };