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.

Revisions

  1. jamiebuilds revised this gist Sep 24, 2024. 2 changed files with 13 additions and 14 deletions.
    17 changes: 8 additions & 9 deletions 1-URLPath.tsx
    Original file line number Diff line number Diff line change
    @@ -1,17 +1,16 @@
    export type URLPathInput = boolean | string | number
    export type URLPathComponentInput = string | URLPath

    export type URLPathParam = Readonly<{
    type URLPathParam = Readonly<{
    key: string,
    value: string,
    }>

    export type URLPathComponent = Readonly<{
    type URLPathComponent = Readonly<{
    text?: string
    param?: URLPathParam
    }>

    export type URLPathComponentInput = string | URLPath

    function escapeURLPathInput(value: URLPathInput): string {
    if (typeof value === "boolean" || typeof value === "number") {
    return String(value);
    @@ -46,7 +45,7 @@ function joinPathname(base: string, next: string) {
    export class URLPath {
    #components: URLPathComponent[]

    #toComponents(inputs: readonly URLPathComponentInput[]) {
    #toComponents(inputs: readonly URLPathComponentInput[]): URLPathComponent[] {
    return inputs.flatMap(component => {
    if (typeof component === "string") {
    return { text: component }
    @@ -67,27 +66,27 @@ export class URLPath {
    }
    }

    append(...inputs: readonly URLPathComponentInput[]) {
    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) {
    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>>) {
    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) {
    toURL(base: string): URL {
    let params: URLPathParam[] = []
    let url = new URL(base)

    10 changes: 5 additions & 5 deletions 2-template-string.tsx
    Original file line number Diff line number Diff line change
    @@ -1,16 +1,16 @@
    export function urlPath(strings: TemplateStringsArray, ...params: readonly (URLPathInput | URLPath)[]) {
    let components: URLPathComponentInput[] = [];
    let inputs: URLPathComponentInput[] = [];
    for (let index = 0; index < strings.length; index += 1) {
    let text = strings[index]
    let param = params[index]
    components.push(text)
    inputs.push(text)
    if (param != null) {
    if (param instanceof URLPath) {
    components.push(param)
    inputs.push(param)
    } else {
    components.push(escapeURLPathInput(param))
    inputs.push(escapeURLPathInput(param))
    }
    }
    }
    return new URLPath().append(...components)
    return new URLPath(...inputs)
    }
  2. jamiebuilds revised this gist Sep 24, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 0-example.ts
    Original file line number Diff line number Diff line change
    @@ -10,7 +10,7 @@
    // URL { "https://example.com/one/two/three/four?v=1&one=1&two=2&three=3&four=4&five=5&six=6" }
    }

    // template string expiriment
    // template string experiment
    {
    let url = urlPath`./one${urlPath`?one=1`}/two`
    .param("two", 2)
  3. jamiebuilds revised this gist Sep 24, 2024. 2 changed files with 9 additions and 7 deletions.
    10 changes: 6 additions & 4 deletions 0-example.ts
    Original file line number Diff line number Diff line change
    @@ -1,18 +1,20 @@
    // Base API
    {
    let url = new URLPath()
    .append('./one', new URLPath("?one=1"), "/two")
    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(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 expiriment
    {
    let url = urlPath`./one${urlPath`?one=1`}/two`
    .param("two", 2)
    .append(urlPath`./three?three=3`, './four?four=4')
    .append(urlPath`./three?three=3`, "./four?four=4")
    .append("?five=5")
    .params({ six: 6 })
    .toURL("https://example.com?v=1")
    6 changes: 3 additions & 3 deletions 1-URLPath.tsx
    Original file line number Diff line number Diff line change
    @@ -13,13 +13,13 @@ export type URLPathComponent = Readonly<{
    export type URLPathComponentInput = string | URLPath

    function escapeURLPathInput(value: URLPathInput): string {
    if (typeof value === 'boolean' || typeof value === 'number') {
    if (typeof value === "boolean" || typeof value === "number") {
    return String(value);
    }
    if (typeof value === 'string') {
    if (typeof value === "string") {
    return encodeURIComponent(value);
    }
    throw new TypeError('Unexpected URLPathInput');
    throw new TypeError("Unexpected URLPathInput");
    }

    function toURLPathParam(key: string, value: URLPathInput): URLPathParam {
  4. jamiebuilds revised this gist Sep 24, 2024. 1 changed file with 20 additions and 7 deletions.
    27 changes: 20 additions & 7 deletions 0-example.ts
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,20 @@
    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")
    // URL { "https://example.com/one/two/three/four?one=1&two=2&three=3&four=4&five=5&six=6" }
    {
    let url = new URLPath()
    .append('./one', 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" }
    }

    {
    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" }
    }
  5. jamiebuilds revised this gist Sep 24, 2024. 2 changed files with 21 additions and 6 deletions.
    11 changes: 5 additions & 6 deletions 1-URLPath.tsx
    Original file line number Diff line number Diff line change
    @@ -1,16 +1,16 @@
    type URLPathInput = boolean | string | number
    export type URLPathInput = boolean | string | number

    type URLPathParam = Readonly<{
    export type URLPathParam = Readonly<{
    key: string,
    value: string,
    }>

    type URLPathComponent = Readonly<{
    export type URLPathComponent = Readonly<{
    text?: string
    param?: URLPathParam
    }>

    type URLPathComponentInput = string | URLPath
    export type URLPathComponentInput = string | URLPath

    function escapeURLPathInput(value: URLPathInput): string {
    if (typeof value === 'boolean' || typeof value === 'number') {
    @@ -43,8 +43,7 @@ function joinPathname(base: string, next: string) {
    return result
    }


    class URLPath {
    export class URLPath {
    #components: URLPathComponent[]

    #toComponents(inputs: readonly URLPathComponentInput[]) {
    16 changes: 16 additions & 0 deletions 2-template-string.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,16 @@
    export function urlPath(strings: TemplateStringsArray, ...params: readonly (URLPathInput | URLPath)[]) {
    let components: URLPathComponentInput[] = [];
    for (let index = 0; index < strings.length; index += 1) {
    let text = strings[index]
    let param = params[index]
    components.push(text)
    if (param != null) {
    if (param instanceof URLPath) {
    components.push(param)
    } else {
    components.push(escapeURLPathInput(param))
    }
    }
    }
    return new URLPath().append(...components)
    }
  6. jamiebuilds revised this gist Sep 24, 2024. 1 changed file with 36 additions and 47 deletions.
    83 changes: 36 additions & 47 deletions 1-URLPath.tsx
    Original file line number Diff line number Diff line change
    @@ -10,7 +10,7 @@ type URLPathComponent = Readonly<{
    param?: URLPathParam
    }>

    type URLPathComponentInput = string | URLPathComponent | URLPath
    type URLPathComponentInput = string | URLPath

    function escapeURLPathInput(value: URLPathInput): string {
    if (typeof value === 'boolean' || typeof value === 'number') {
    @@ -22,18 +22,6 @@ function escapeURLPathInput(value: URLPathInput): string {
    throw new TypeError('Unexpected URLPathInput');
    }

    function toURLPathComponents(inputs: readonly URLPathComponentInput[]) {
    return inputs.flatMap(component => {
    if (typeof component === "string") {
    return { text: component }
    } else if (component instanceof URLPath) {
    return component[COMPONENTS]
    } else {
    return component
    }
    })
    }

    function toURLPathParam(key: string, value: URLPathInput): URLPathParam {
    return { key: encodeURIComponent(key), value: escapeURLPathInput(value) }
    }
    @@ -55,39 +43,59 @@ function joinPathname(base: string, next: string) {
    return result
    }

    const COMPONENTS = Symbol("components")

    class URLPath {
    #components: URLPathComponent[]

    get [COMPONENTS]() {
    return this.#components

    #toComponents(inputs: readonly URLPathComponentInput[]) {
    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[]) {
    this.#components = toURLPathComponents(inputs)

    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[]) {
    console.log(toURLPathComponents(inputs))
    return new URLPath(this.#components.concat(toURLPathComponents(inputs)))
    let path = new URLPath(this)
    path.#components = path.#components.concat(this.#toComponents(inputs))
    return path
    }

    param(key: string, value: URLPathInput) {
    return new URLPath(this.#components.concat({ param: toURLPathParam(key, value) }))
    let path = new URLPath(this)
    path.#components.push({ param: toURLPathParam(key, value) })
    return path
    }

    params(params: Readonly<Record<string, URLPathInput>>) {
    let components: URLPathComponent[] = []
    let path = new URLPath(this)
    for (let [key, value] of Object.entries(params)) {
    components.push({ param: toURLPathParam(key, value) })
    path.#components.push({ param: toURLPathParam(key, value) })
    }
    return new URLPath(this.#components.concat(components))
    return path
    }

    toURL(base: string) {
    let url = new URL(base)
    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)
    @@ -107,23 +115,4 @@ class URLPath {

    return result
    }
    }

    export type { URLPath };

    export function urlPath(strings: TemplateStringsArray, ...params: readonly (URLPathInput | URLPath)[]) {
    let components: URLPathComponent[] = [];
    for (let index = 0; index < strings.length; index += 1) {
    let text = strings[index]
    let param = params[index]
    components.push({ text })
    if (param != null) {
    if (param instanceof URLPath) {
    components = components.concat(param[COMPONENTS])
    } else {
    components.push({ text: escapeURLPathInput(param) })
    }
    }
    }
    return new URLPath(components);
    }
    }
  7. jamiebuilds created this gist Sep 24, 2024.
    7 changes: 7 additions & 0 deletions 0-example.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    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")
    // URL { "https://example.com/one/two/three/four?one=1&two=2&three=3&four=4&five=5&six=6" }
    129 changes: 129 additions & 0 deletions 1-URLPath.tsx
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,129 @@
    type URLPathInput = boolean | string | number

    type URLPathParam = Readonly<{
    key: string,
    value: string,
    }>

    type URLPathComponent = Readonly<{
    text?: string
    param?: URLPathParam
    }>

    type URLPathComponentInput = string | URLPathComponent | URLPath

    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 toURLPathComponents(inputs: readonly URLPathComponentInput[]) {
    return inputs.flatMap(component => {
    if (typeof component === "string") {
    return { text: component }
    } else if (component instanceof URLPath) {
    return component[COMPONENTS]
    } else {
    return component
    }
    })
    }

    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
    }

    const COMPONENTS = Symbol("components")

    class URLPath {
    #components: URLPathComponent[]

    get [COMPONENTS]() {
    return this.#components
    }

    constructor(inputs: readonly URLPathComponentInput[]) {
    this.#components = toURLPathComponents(inputs)
    }

    append(...inputs: readonly URLPathComponentInput[]) {
    console.log(toURLPathComponents(inputs))
    return new URLPath(this.#components.concat(toURLPathComponents(inputs)))
    }

    param(key: string, value: URLPathInput) {
    return new URLPath(this.#components.concat({ param: toURLPathParam(key, value) }))
    }

    params(params: Readonly<Record<string, URLPathInput>>) {
    let components: URLPathComponent[] = []
    for (let [key, value] of Object.entries(params)) {
    components.push({ param: toURLPathParam(key, value) })
    }
    return new URLPath(this.#components.concat(components))
    }

    toURL(base: string) {
    let url = new URL(base)
    let params: URLPathParam[] = []
    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 type { URLPath };

    export function urlPath(strings: TemplateStringsArray, ...params: readonly (URLPathInput | URLPath)[]) {
    let components: URLPathComponent[] = [];
    for (let index = 0; index < strings.length; index += 1) {
    let text = strings[index]
    let param = params[index]
    components.push({ text })
    if (param != null) {
    if (param instanceof URLPath) {
    components = components.concat(param[COMPONENTS])
    } else {
    components.push({ text: escapeURLPathInput(param) })
    }
    }
    }
    return new URLPath(components);
    }