Last active
September 24, 2024 18:54
-
-
Save jamiebuilds/c6d8c8cdf7631a0e0d4b6d6f4b69924c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Base API | |
{ | |
let url = new URLPath("./one") | |
.append(new URLPath("?one=1"), "/two") | |
.param("two", 2) | |
.append(new URLPath().append(`./three?three=3`), "./four?four=4") | |
.append("?five=5") | |
.params({ six: 6 }) | |
.toURL("https://example.com?v=1") | |
// URL { "https://example.com/one/two/three/four?v=1&one=1&two=2&three=3&four=4&five=5&six=6" } | |
} | |
// template string experiment | |
{ | |
let url = urlPath`./one${urlPath`?one=1`}/two` | |
.param("two", 2) | |
.append(urlPath`./three?three=3`, "./four?four=4") | |
.append("?five=5") | |
.params({ six: 6 }) | |
.toURL("https://example.com?v=1") | |
// URL { "https://example.com/one/two/three/four?v=1&one=1&two=2&three=3&four=4&five=5&six=6" } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type URLPathInput = boolean | string | number | |
export type URLPathComponentInput = string | URLPath | |
type URLPathParam = Readonly<{ | |
key: string, | |
value: string, | |
}> | |
type URLPathComponent = Readonly<{ | |
text?: string | |
param?: URLPathParam | |
}> | |
function escapeURLPathInput(value: URLPathInput): string { | |
if (typeof value === "boolean" || typeof value === "number") { | |
return String(value); | |
} | |
if (typeof value === "string") { | |
return encodeURIComponent(value); | |
} | |
throw new TypeError("Unexpected URLPathInput"); | |
} | |
function toURLPathParam(key: string, value: URLPathInput): URLPathParam { | |
return { key: encodeURIComponent(key), value: escapeURLPathInput(value) } | |
} | |
function joinPathname(base: string, next: string) { | |
let result = "" | |
if (base !== "/") { | |
result += base | |
} | |
if ( | |
!result.endsWith("/") && | |
!next.startsWith("/") && | |
!next.startsWith("?") && | |
!next.startsWith("#") | |
) { | |
result += "/" | |
} | |
result += next | |
return result | |
} | |
export class URLPath { | |
#components: URLPathComponent[] | |
#toComponents(inputs: readonly URLPathComponentInput[]): URLPathComponent[] { | |
return inputs.flatMap(component => { | |
if (typeof component === "string") { | |
return { text: component } | |
} else if (component instanceof URLPath) { | |
return component.#components | |
} else { | |
throw new Error("Invalid input") | |
} | |
}) | |
} | |
constructor(...inputs: readonly URLPathComponentInput[]) { | |
if (inputs.length === 1 && inputs[0] instanceof URLPath) { | |
// fast path | |
this.#components = inputs[0].#components | |
} else { | |
this.#components = this.#toComponents(inputs) | |
} | |
} | |
append(...inputs: readonly URLPathComponentInput[]): URLPath { | |
let path = new URLPath(this) | |
path.#components = path.#components.concat(this.#toComponents(inputs)) | |
return path | |
} | |
param(key: string, value: URLPathInput): URLPath { | |
let path = new URLPath(this) | |
path.#components.push({ param: toURLPathParam(key, value) }) | |
return path | |
} | |
params(params: Readonly<Record<string, URLPathInput>>): URLPath { | |
let path = new URLPath(this) | |
for (let [key, value] of Object.entries(params)) { | |
path.#components.push({ param: toURLPathParam(key, value) }) | |
} | |
return path | |
} | |
toURL(base: string): URL { | |
let params: URLPathParam[] = [] | |
let url = new URL(base) | |
for (let [key, value] of url.searchParams) { | |
params.push({ key, value }) | |
} | |
for (let component of this.#components) { | |
if (component.text != null) { | |
url = new URL(joinPathname(url.pathname, component.text), url.origin) | |
for (let [key, value] of url.searchParams) { | |
params.push({ key, value }) | |
} | |
} | |
if (component.param != null) { | |
params.push(component.param) | |
} | |
} | |
let result = new URL(`${url.origin}${url.pathname}`) | |
for (let param of params) { | |
result.searchParams.set(param.key, param.value) | |
} | |
return result | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function urlPath(strings: TemplateStringsArray, ...params: readonly (URLPathInput | URLPath)[]) { | |
let inputs: URLPathComponentInput[] = []; | |
for (let index = 0; index < strings.length; index += 1) { | |
let text = strings[index] | |
let param = params[index] | |
inputs.push(text) | |
if (param != null) { | |
if (param instanceof URLPath) { | |
inputs.push(param) | |
} else { | |
inputs.push(escapeURLPathInput(param)) | |
} | |
} | |
} | |
return new URLPath(...inputs) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment