@wevu/web-apis
Version:
Web API polyfills and global installers for mini-program runtimes
210 lines (209 loc) • 6.58 kB
JavaScript
//#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 };