diff --git a/src/utils.ts b/src/utils.ts index 4a8aa5c0..8fa951bf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -49,10 +49,13 @@ export function isScriptProtocol(protocol?: string) { return !!protocol && PROTOCOL_SCRIPT_RE.test(protocol); } -const TRAILING_SLASH_RE = /\/$|\/\?/; +const TRAILING_SLASH_RE = /\/$|\/\?|\/#/; -export function hasTrailingSlash(input = "", queryParameters = false): boolean { - if (!queryParameters) { +export function hasTrailingSlash( + input = "", + respectQueryAndFragment?: boolean +): boolean { + if (!respectQueryAndFragment) { return input.endsWith("/"); } return TRAILING_SLASH_RE.test(input); @@ -60,27 +63,51 @@ export function hasTrailingSlash(input = "", queryParameters = false): boolean { export function withoutTrailingSlash( input = "", - queryParameters = false + respectQueryAndFragment?: boolean ): string { - if (!queryParameters) { + if (!respectQueryAndFragment) { return (hasTrailingSlash(input) ? input.slice(0, -1) : input) || "/"; } if (!hasTrailingSlash(input, true)) { return input || "/"; } - const [s0, ...s] = input.split("?"); - return (s0.slice(0, -1) || "/") + (s.length > 0 ? `?${s.join("?")}` : ""); + let path = input; + let fragment = ""; + const fragmentIndex = input.indexOf("#"); + if (fragmentIndex >= 0) { + path = input.slice(0, fragmentIndex); + fragment = input.slice(fragmentIndex); + } + const [s0, ...s] = path.split("?"); + return ( + (s0.slice(0, -1) || "/") + + (s.length > 0 ? `?${s.join("?")}` : "") + + fragment + ); } -export function withTrailingSlash(input = "", queryParameters = false): string { - if (!queryParameters) { +export function withTrailingSlash( + input = "", + respectQueryAndFragment?: boolean +): string { + if (!respectQueryAndFragment) { return input.endsWith("/") ? input : input + "/"; } if (hasTrailingSlash(input, true)) { return input || "/"; } - const [s0, ...s] = input.split("?"); - return s0 + "/" + (s.length > 0 ? `?${s.join("?")}` : ""); + let path = input; + let fragment = ""; + const fragmentIndex = input.indexOf("#"); + if (fragmentIndex >= 0) { + path = input.slice(0, fragmentIndex); + fragment = input.slice(fragmentIndex); + if (!path) { + return fragment; + } + } + const [s0, ...s] = path.split("?"); + return s0 + "/" + (s.length > 0 ? `?${s.join("?")}` : "") + fragment; } export function hasLeadingSlash(input = ""): boolean { diff --git a/test/trailing-slash.test.ts b/test/trailing-slash.test.ts index c116f7e2..21a1e37b 100644 --- a/test/trailing-slash.test.ts +++ b/test/trailing-slash.test.ts @@ -5,9 +5,11 @@ describe("withTrailingSlash, queryParams: false", () => { const tests = { "": "/", bar: "bar/", + "bar#abc": "bar#abc/", "bar/": "bar/", "foo?123": "foo?123/", "foo/?123": "foo/?123/", + "foo/?123#abc": "foo/?123#abc/", }; for (const input in tests) { @@ -28,6 +30,10 @@ describe("withTrailingSlash, queryParams: true", () => { "bar/": "bar/", "foo?123": "foo/?123", "foo/?123": "foo/?123", + "foo?123#abc": "foo/?123#abc", + "/#abc": "/#abc", + "#abc": "#abc", + "#": "#", }; for (const input in tests) { @@ -46,9 +52,11 @@ describe("withoutTrailingSlash, queryParams: false", () => { "": "/", "/": "/", bar: "bar", - "bar/": "bar", + "bar#abc": "bar#abc", + "bar/#abc": "bar/#abc", "foo?123": "foo?123", "foo/?123": "foo/?123", + "foo/?123#abc": "foo/?123#abc", }; for (const input in tests) { @@ -68,8 +76,13 @@ describe("withoutTrailingSlash, queryParams: true", () => { "/": "/", bar: "bar", "bar/": "bar", + "bar#abc": "bar#abc", + "bar/#abc": "bar#abc", "foo?123": "foo?123", "foo/?123": "foo?123", + "foo/?123#abc": "foo?123#abc", + "/a/#abc": "/a#abc", + "/#abc": "/#abc", }; for (const input in tests) {