From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../media-api/src/models/urls.ts | 469 +++++++++++++++++++++ 1 file changed, 469 insertions(+) create mode 100644 node_modules/@apple-media-services/media-api/src/models/urls.ts (limited to 'node_modules/@apple-media-services/media-api/src/models/urls.ts') diff --git a/node_modules/@apple-media-services/media-api/src/models/urls.ts b/node_modules/@apple-media-services/media-api/src/models/urls.ts new file mode 100644 index 0000000..c1c09f1 --- /dev/null +++ b/node_modules/@apple-media-services/media-api/src/models/urls.ts @@ -0,0 +1,469 @@ +/** + * Created by keithpk on 12/2/16. + */ + +import { isNothing, Nothing, Opt } from "@jet/environment/types/optional"; +import { isDefinedNonNullNonEmpty, isNullOrEmpty } from "./server-data"; + +export type Query = { + [key: string]: string | undefined; +}; + +export type URLComponent = "protocol" | "username" | "password" | "host" | "port" | "pathname" | "query" | "hash"; + +const protocolRegex = /^([a-z][a-z0-9.+-]*:)(\/\/)?([\S\s]*)/i; +const queryParamRegex = /([^=?&]+)=?([^&]*)/g; +const componentOrder: URLComponent[] = ["hash", "query", "pathname", "host"]; + +type URLSplitStyle = "prefix" | "suffix"; + +type URLSplitResult = { + result?: string; + remainder: string; +}; + +function splitUrlComponent(input: string, marker: string, style: URLSplitStyle): URLSplitResult { + const index = input.indexOf(marker); + let result; + let remainder = input; + if (index !== -1) { + const prefix = input.slice(0, index); + const suffix = input.slice(index + marker.length, input.length); + + if (style === "prefix") { + result = prefix; + remainder = suffix; + } else { + result = suffix; + remainder = prefix; + } + } + + // log("Token: " + marker + " String: " + input, " Result: " + result + " Remainder: " + remainder) + + return { + result: result, + remainder: remainder, + }; +} + +export class URL { + protocol?: Opt; + username: string; + password: string; + host?: Opt; + port: string; + pathname?: Opt; + query?: Query = {}; + hash?: string; + + constructor(url?: string) { + if (isNullOrEmpty(url)) { + return; + } + + // Split the protocol from the rest of the urls + let remainder = url; + const match = protocolRegex.exec(url); + if (match != null) { + // Pull out the protocol + let protocol = match[1]; + if (protocol) { + protocol = protocol.split(":")[0]; + } + + this.protocol = protocol; + + // Save the remainder + remainder = match[3]; + } + + // Then match each component in a specific order + let parse: URLSplitResult = { remainder: remainder, result: undefined }; + for (const component of componentOrder) { + if (!parse.remainder) { + break; + } + + switch (component) { + case "hash": { + parse = splitUrlComponent(parse.remainder, "#", "suffix"); + this.hash = parse.result; + break; + } + case "query": { + parse = splitUrlComponent(parse.remainder, "?", "suffix"); + if (isDefinedNonNullNonEmpty(parse.result)) { + this.query = URL.queryFromString(parse.result); + } + break; + } + case "pathname": { + parse = splitUrlComponent(parse.remainder, "/", "suffix"); + + if (isDefinedNonNullNonEmpty(parse.result)) { + // Replace the initial /, since paths require it + this.pathname = "/" + parse.result; + } + break; + } + case "host": { + if (parse.remainder) { + const authorityParse = splitUrlComponent(parse.remainder, "@", "prefix"); + const userInfo = authorityParse.result; + const hostPort = authorityParse.remainder; + if (isDefinedNonNullNonEmpty(userInfo)) { + const userInfoSplit = userInfo.split(":"); + this.username = decodeURIComponent(userInfoSplit[0]); + this.password = decodeURIComponent(userInfoSplit[1]); + } + + if (hostPort) { + const hostPortSplit = hostPort.split(":"); + this.host = hostPortSplit[0]; + this.port = hostPortSplit[1]; + } + } + break; + } + default: { + throw new Error("Unhandled case!"); + } + } + } + } + + set(component: URLComponent, value: string | Query): URL { + if (isNullOrEmpty(value)) { + return this; + } + + if (component === "query") { + if (typeof value === "string") { + value = URL.queryFromString(value); + } + } + + switch (component) { + // Exhaustive match to make sure TS property minifiers and other + // transformer plugins do not break this code. + case "protocol": + this.protocol = value as string; + break; + case "username": + this.username = value as string; + break; + case "password": + this.password = value as string; + break; + case "port": + this.port = value as string; + break; + case "pathname": + this.pathname = value as string; + break; + case "query": + this.query = value as Query; + break; + case "hash": + this.hash = value as string; + break; + default: + // The fallback for component which is not a property of URL object. + this[component] = value as string; + break; + } + return this; + } + + private get(component: URLComponent): string | Query | Nothing { + switch (component) { + // Exhaustive match to make sure TS property minifiers and other + // transformer plugins do not break this code. + case "protocol": + return this.protocol; + case "username": + return this.username; + case "password": + return this.password; + case "port": + return this.port; + case "pathname": + return this.pathname; + case "query": + return this.query; + case "hash": + return this.hash; + default: + // The fallback for component which is not a property of URL object. + return this[component]; + } + } + + append(component: URLComponent, value: string | Query): URL { + const existingValue = this.get(component); + let newValue; + + if (component === "query") { + if (typeof value === "string") { + value = URL.queryFromString(value); + } + + if (typeof existingValue === "string") { + newValue = { existingValue, ...value }; + } else { + newValue = { ...existingValue, ...value }; + } + } else { + let existingValueString = existingValue as string; + + if (!existingValueString) { + existingValueString = ""; + } + + let newValueString = existingValueString; + + if (component === "pathname") { + const pathLength = existingValueString.length; + if (!pathLength || existingValueString[pathLength - 1] !== "/") { + newValueString += "/"; + } + } + + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-base-to-string + newValueString += value; + newValue = newValueString; + } + + return this.set(component, newValue); + } + + param(key: string, value?: string): URL { + if (!key) { + return this; + } + if (this.query == null) { + this.query = {}; + } + this.query[key] = value; + return this; + } + + removeParam(key: string): URL { + if (!key || this.query == null) { + return this; + } + if (this.query[key] !== undefined) { + delete this.query[key]; + } + return this; + } + + /** + * Push a new string value onto the path for this url + * @returns URL this object with the updated path. + */ + path(value: string): URL { + return this.append("pathname", value); + } + + pathExtension(): Opt { + // Extract path extension if one exists + if (isNothing(this.pathname)) { + return null; + } + + const lastFilenameComponents = this.pathname + .split("/") + .filter((item) => item.length > 0) // Remove any double or trailing slashes + .pop() + ?.split("."); + if (lastFilenameComponents === undefined) { + return null; + } + if ( + lastFilenameComponents.filter((part) => { + return part !== ""; + }).length < 2 // Remove any empty parts (e.g. .ssh_config -> ["ssh_config"]) + ) { + return null; + } + + return lastFilenameComponents.pop(); + } + + /** + * Returns the path components of the URL + * @returns An array of non-empty path components from `urls`. + */ + pathComponents(): string[] { + if (isNullOrEmpty(this.pathname)) { + return []; + } + + return this.pathname.split("/").filter((component) => component.length > 0); + } + + /** + * Returns the last path component from this url, updating the url to not include this path component + * @returns String the last path component from this url. + */ + popPathComponent(): string | null { + if (isNullOrEmpty(this.pathname)) { + return null; + } + + const lastPathComponent = this.pathname.slice(this.pathname.lastIndexOf("/") + 1); + + if (lastPathComponent.length === 0) { + return null; + } + + this.pathname = this.pathname.slice(0, this.pathname.lastIndexOf("/")); + + return lastPathComponent; + } + + /** + * Same as toString + * + * @returns {string} A string representation of the URL + */ + build(): string { + return this.toString(); + } + + /** + * Converts the URL to a string + * + * @returns {string} A string representation of the URL + */ + toString(): string { + let url = ""; + + if (isDefinedNonNullNonEmpty(this.protocol)) { + url += this.protocol + "://"; + } + + if (this.username) { + url += encodeURIComponent(this.username); + + if (this.password) { + url += ":" + encodeURIComponent(this.password); + } + + url += "@"; + } + + if (isDefinedNonNullNonEmpty(this.host)) { + url += this.host; + + if (this.port) { + url += ":" + this.port; + } + } + + if (isDefinedNonNullNonEmpty(this.pathname)) { + url += this.pathname; + /// Trim off trailing path separators when we have a valid path + /// We don't do this unless pathname has elements otherwise we will trim the `://` + if (url.endsWith("/") && this.pathname.length > 0) { + url = url.slice(0, -1); + } + } + + if (this.query != null && Object.keys(this.query).length > 0) { + url += "?" + URL.toQueryString(this.query); + } + + if (isDefinedNonNullNonEmpty(this.hash)) { + url += "#" + this.hash; + } + + return url; + } + + // ---------------- + // Static API + // ---------------- + + /** + * Converts a string into a query dictionary + * @param query The string to parse + * @returns The query dictionary containing the key-value pairs in the query string + */ + static queryFromString(query: string): Query { + const result = {}; + + let parseResult = queryParamRegex.exec(query); + while (parseResult != null) { + const key = decodeURIComponent(parseResult[1]); + const value = decodeURIComponent(parseResult[2]); + result[key] = value; + parseResult = queryParamRegex.exec(query); + } + + return result; + } + + /** + * Converts a query dictionary into a query string + * + * @param query The query dictionary + * @returns {string} The string representation of the query dictionary + */ + static toQueryString(query: Query) { + let queryString = ""; + + let first = true; + for (const key of Object.keys(query)) { + if (!first) { + queryString += "&"; + } + first = false; + + queryString += encodeURIComponent(key); + + const value = query[key]; + if (isDefinedNonNullNonEmpty(value) && value.length) { + queryString += "=" + encodeURIComponent(value); + } + } + + return queryString; + } + + /** + * Convenience method to instantiate a URL from a string + * @param url The URL string to parse + * @returns {URL} The new URL object representing the URL + */ + static from(url: string): URL { + return new URL(url); + } + + /** + * Convenience method to instantiate a URL from numerous (optional) components + * @param protocol The protocol type + * @param host The host name + * @param path The path + * @param query The query + * @param hash The hash + * @returns {URL} The new URL object representing the URL + */ + static fromComponents( + protocol?: Opt, + host?: Opt, + path?: Opt, + query?: Query, + hash?: string, + ): URL { + const url = new URL(); + url.protocol = protocol; + url.host = host; + url.pathname = path; + url.query = query; + url.hash = hash; + return url; + } +} -- cgit v1.2.3