All files / owid-grapher/clientUtils/urls Url.ts

98.37% Statements 121/123
82.14% Branches 23/28
100% Functions 18/18
98.37% Lines 121/123

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 1681x 1x 1x 1x 1x 1x 1x 10x 10x 10x 10x 10x 10x 10x 10x 1x 1x 923x 218x 218x 1x 1x 923x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 10x 10x 10x 10x 10x 10x 10x 10x 10x 1x 1x 246x 246x 246x 246x 1x 1x 367x 367x 367x 367x 1x 1x 734x 734x 734x 734x   734x   734x 734x 734x 1x 1x 4x 4x 1x 1x 27x 27x 1x 1x 4x 4x 1x 1x 3x 3x 2x 2x 1x 1x 2206x 2206x 2206x 1529x 1529x 2206x 1x 1x 4x 4x 4x 1x 1x 4x 4x 4x 4x 4x 4x 4x 1x 1x 1593x 1593x 1x 1x 613x 613x 1x 1x 8x                                                                                          
import urlParseLib from "url-parse"
 
import { excludeUndefined, omitUndefinedValues } from "../Util"
 
import { QueryParams, queryParamsToStr, strToQueryParams } from "./UrlUtils"
 
const parseUrl = (url: string) => {
    const parsed = urlParseLib(url, {})
    // The library returns an unparsed string for `query`, its types aren't quite right.
    const query = parsed.query.toString()
    return {
        ...parsed,
        query,
    }
}
 
const ensureStartsWith = (str: string, start: string): string => {
    if (str.startsWith(start)) return str
    return `${start}${str}`
}
 
const ensureQueryStrFormat = (queryStr: string) =>
    ensureStartsWith(queryStr, "?")
const ensureHashFormat = (queryStr: string) => ensureStartsWith(queryStr, "#")
 
interface UrlProps {
    readonly origin?: string // https://ourworldindata.org
    readonly pathname?: string // /grapher/abc
    readonly queryStr?: string // ?stackMode=relative
    readonly hash?: string // #articles
}
 
export class Url {
    private props: UrlProps
 
    /**
     * @param url Absolute or relative URL
     */
    static fromURL(url: string) {
        const { origin, pathname, query, hash } = parseUrl(url)
        return new Url({
            origin:
                origin !== undefined && origin !== "null" ? origin : undefined,
            pathname,
            queryStr: query,
            hash,
        })
    }
 
    static fromQueryStr(queryStr: string) {
        return new Url({
            queryStr: ensureQueryStrFormat(queryStr),
        })
    }
 
    static fromQueryParams(queryParams: QueryParams) {
        return new Url({
            queryStr: queryParamsToStr(queryParams),
        })
    }
 
    private constructor(props: UrlProps = {}) {
        this.props = {
            ...props,
            pathname:
                props.pathname !== undefined
                    ? props.pathname
                    : props.origin
                    ? ""
                    : undefined,
        }
    }
 
    get origin(): string | undefined {
        return this.props.origin
    }
 
    get pathname(): string | undefined {
        return this.props.pathname
    }
 
    get slug(): string | undefined {
        return this.props.pathname?.replace(/^\/+/, "")
    }
 
    get originAndPath(): string | undefined {
        const strings = excludeUndefined([this.origin, this.pathname])
        if (strings.length === 0) return undefined
        return strings.join("")
    }
 
    get queryStr(): string {
        const { queryStr } = this.props
        // Drop a single trailing `?`, if there is one
        return queryStr && queryStr !== "?"
            ? ensureQueryStrFormat(queryStr)
            : ""
    }
 
    get hash(): string {
        const { hash } = this.props
        return hash ? ensureHashFormat(hash) : ""
    }
 
    get fullUrl(): string {
        return excludeUndefined([
            this.origin,
            this.pathname,
            this.queryStr,
            this.hash,
        ]).join("")
    }
 
    get queryParams(): QueryParams {
        return strToQueryParams(this.queryStr)
    }
 
    get encodedQueryParams(): QueryParams {
        return strToQueryParams(this.queryStr, true)
    }
 
    get isGrapher(): boolean {
        return this.pathname ? /^\/grapher\//.test(this.pathname) : false
    }
 
    get isUpload(): boolean {
        return this.pathname ? /^\/uploads\//.test(this.pathname) : false
    }
 
    update(props: UrlProps) {
        return new Url({
            ...this.props,
            ...props,
        })
    }
 
    setQueryParams(queryParams: QueryParams) {
        return new Url({
            ...this.props,
            queryStr: queryParamsToStr(queryParams),
        })
    }
 
    updateQueryParams(queryParams: QueryParams) {
        return this.update({
            queryStr: queryParamsToStr(
                omitUndefinedValues({
                    ...this.queryParams,
                    ...queryParams,
                })
            ),
        })
    }
}
 
export const setWindowUrl = (url: Url): void => {
    const pathname = url.pathname ?? window.location.pathname
    window.history.replaceState(
        null,
        document.title,
        excludeUndefined([pathname, url.queryStr, url.hash]).join("")
    )
}
 
export const getWindowUrl = (): Url => {
    return Url.fromURL(window.location.href)
}