Skip to content

Instantly share code, notes, and snippets.

@jamiebuilds
Last active September 24, 2024 18:54
Show Gist options
  • Save jamiebuilds/c6d8c8cdf7631a0e0d4b6d6f4b69924c to your computer and use it in GitHub Desktop.
Save jamiebuilds/c6d8c8cdf7631a0e0d4b6d6f4b69924c to your computer and use it in GitHub Desktop.
// 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" }
}
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
}
}
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