diff --git a/src/lib/kernel/Redirector.svelte.test.ts b/src/lib/Redirector.svelte.test.ts similarity index 99% rename from src/lib/kernel/Redirector.svelte.test.ts rename to src/lib/Redirector.svelte.test.ts index 47dfd78..ea39d17 100644 --- a/src/lib/kernel/Redirector.svelte.test.ts +++ b/src/lib/Redirector.svelte.test.ts @@ -3,9 +3,9 @@ import { testWithEffect as test } from "$test/testWithEffect.svelte.js"; import { ALL_HASHES, ROUTING_UNIVERSES } from "$test/test-utils.js"; import { init } from "$lib/init.js"; import type { Hash, PatternRouteInfo, RedirectedRouteInfo } from "$lib/types.js"; -import { resolveHashValue } from "./resolveHashValue.js"; +import { resolveHashValue } from "./kernel/resolveHashValue.js"; import { Redirector } from "./Redirector.svelte.js"; -import { location } from "./Location.js"; +import { location } from "./kernel/Location.js"; import { flushSync } from "svelte"; ROUTING_UNIVERSES.forEach((universe) => { diff --git a/src/lib/kernel/Redirector.svelte.ts b/src/lib/Redirector.svelte.ts similarity index 95% rename from src/lib/kernel/Redirector.svelte.ts rename to src/lib/Redirector.svelte.ts index be1fd41..7995e8d 100644 --- a/src/lib/kernel/Redirector.svelte.ts +++ b/src/lib/Redirector.svelte.ts @@ -1,7 +1,7 @@ import type { Hash, ParameterValue, RedirectedRouteInfo } from "$lib/types.js"; -import { RouteHelper } from "./RouteHelper.svelte.js"; -import { location } from "./Location.js"; -import { resolveHashValue } from "./resolveHashValue.js"; +import { RouteHelper } from "./kernel/RouteHelper.svelte.js"; +import { location } from "./kernel/Location.js"; +import { resolveHashValue } from "./kernel/resolveHashValue.js"; import { untrack } from "svelte"; /** @@ -124,7 +124,7 @@ export class Redirector { const url = typeof redirection.href === 'function' ? redirection.href(routeParams) : redirection.href; - location[(redirection.goTo ? 'goTo' : 'navigate')](url, { + location[redirection.goTo ? 'goTo' : 'navigate'](url, { hash: this.#hash, replace: this.#options.replace, ...redirection.options, diff --git a/src/lib/index.test.ts b/src/lib/index.test.ts index f7481fd..6191ae2 100644 --- a/src/lib/index.test.ts +++ b/src/lib/index.test.ts @@ -21,6 +21,7 @@ describe('index', () => { 'activeBehavior', 'Redirector', 'buildHref', + 'joinPaths', ]; // Act. diff --git a/src/lib/index.ts b/src/lib/index.ts index 6ad9a5a..0e99f92 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -14,5 +14,5 @@ export * from './RouterTrace/RouterTrace.svelte'; export { default as RouterTrace } from './RouterTrace/RouterTrace.svelte'; export * from "./public-utils.js"; export * from "./behaviors/active.svelte.js"; -export { Redirector } from "./kernel/Redirector.svelte.js"; +export { Redirector } from "./Redirector.svelte.js"; export { buildHref } from "./buildHref.js"; diff --git a/src/lib/kernel/RouteHelper.svelte.ts b/src/lib/kernel/RouteHelper.svelte.ts index 11b64a2..505002a 100644 --- a/src/lib/kernel/RouteHelper.svelte.ts +++ b/src/lib/kernel/RouteHelper.svelte.ts @@ -1,33 +1,8 @@ +import { joinPaths } from "$lib/public-utils.js"; import type { AndUntyped, Hash, PatternRouteInfo, RouteStatus } from "$lib/types.js"; +import { noTrailingSlash } from "$lib/utils.js"; import { location } from "./Location.js"; -function noTrailingSlash(path: string) { - return path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path; -} - -function hasLeadingSlash(paths: (string | undefined)[]) { - for (let path of paths) { - if (!path) { - continue; - } - return path.startsWith('/'); - } - return false; -} - -/** - * Joins the provided paths into a single path. - * @param paths Paths to join. - * @returns The joined path. - */ -export function joinPaths(...paths: string[]) { - const result = paths.reduce((acc, path, index) => { - const trimmedPath = (path ?? '').replace(/^\/|\/$/g, ''); - return acc + (index > 0 && !acc.endsWith('/') && trimmedPath.length > 0 ? '/' : '') + trimmedPath; - }, hasLeadingSlash(paths) ? '/' : ''); - return noTrailingSlash(result); -} - function escapeRegExp(string: string): string { return string.replace(/[.+^${}()|[\]\\]/g, '\\$&'); } diff --git a/src/lib/kernel/RouterEngine.svelte.ts b/src/lib/kernel/RouterEngine.svelte.ts index 82f43bb..44611de 100644 --- a/src/lib/kernel/RouterEngine.svelte.ts +++ b/src/lib/kernel/RouterEngine.svelte.ts @@ -4,7 +4,8 @@ import { location } from "./Location.js"; import { routingOptions } from "./options.js"; import { resolveHashValue } from "./resolveHashValue.js"; import { assertAllowedRoutingMode } from "$lib/utils.js"; -import { joinPaths, RouteHelper } from "./RouteHelper.svelte.js"; +import { RouteHelper } from "./RouteHelper.svelte.js"; +import { joinPaths } from "$lib/public-utils.js"; /** * RouterEngine's options. diff --git a/src/lib/kernel/calculateHref.ts b/src/lib/kernel/calculateHref.ts index eab8b40..fa99357 100644 --- a/src/lib/kernel/calculateHref.ts +++ b/src/lib/kernel/calculateHref.ts @@ -2,7 +2,7 @@ import type { Hash, PreserveQuery } from "../types.js"; import { dissectHrefs } from "./dissectHrefs.js"; import { location } from "./Location.js"; import { mergeQueryParams } from "./preserveQuery.js"; -import { joinPaths } from "./RouteHelper.svelte.js"; +import { joinPaths } from "$lib/public-utils.js"; import { resolveHashValue } from "./resolveHashValue.js"; import { calculateMultiHashFragment } from "./calculateMultiHashFragment.js"; diff --git a/src/lib/kernel/index.test.ts b/src/lib/kernel/index.test.ts index b33bde1..3c3321d 100644 --- a/src/lib/kernel/index.test.ts +++ b/src/lib/kernel/index.test.ts @@ -6,7 +6,6 @@ describe('index', () => { const expectedList = [ 'location', 'RouterEngine', - 'joinPaths', 'isConformantState', 'calculateHref', 'calculateState', diff --git a/src/lib/kernel/index.ts b/src/lib/kernel/index.ts index fee3d66..31dfa5b 100644 --- a/src/lib/kernel/index.ts +++ b/src/lib/kernel/index.ts @@ -1,6 +1,5 @@ export { location } from "./Location.js"; export { RouterEngine } from "./RouterEngine.svelte.js"; -export { joinPaths } from "./RouteHelper.svelte.js"; export { isConformantState } from "./isConformantState.js"; export { calculateHref } from "./calculateHref.js"; export { calculateMultiHashFragment } from "./calculateMultiHashFragment.js"; diff --git a/src/lib/public-utils.ts b/src/lib/public-utils.ts index d4405e1..879037b 100644 --- a/src/lib/public-utils.ts +++ b/src/lib/public-utils.ts @@ -1,5 +1,6 @@ import { RouterEngine } from "./kernel/RouterEngine.svelte.js"; import type { RouteStatus } from "./types.js"; +import { noTrailingSlash } from "./utils.js"; /** * Checks if a specific route is active according to the provided router engine or route status record. @@ -16,3 +17,26 @@ export function isRouteActive( const rs = rsOrRouter instanceof RouterEngine ? rsOrRouter.routeStatus : rsOrRouter; return !!rs?.[key ?? '']?.match; } + +function hasLeadingSlash(paths: (string | undefined)[]) { + for (let path of paths) { + if (!path) { + continue; + } + return path.startsWith('/'); + } + return false; +} + +/** + * Joins the provided paths into a single path. + * @param paths Paths to join. + * @returns The joined path. + */ +export function joinPaths(...paths: string[]) { + const result = paths.reduce((acc, path, index) => { + const trimmedPath = (path ?? '').replace(/^\/|\/$/g, ''); + return acc + (index > 0 && !acc.endsWith('/') && trimmedPath.length > 0 ? '/' : '') + trimmedPath; + }, hasLeadingSlash(paths) ? '/' : ''); + return noTrailingSlash(result); +} diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index 6f77e2e..354fbb2 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test, vi } from "vitest"; -import { assertAllowedRoutingMode, expandAriaAttributes } from "./utils.js"; +import { assertAllowedRoutingMode, expandAriaAttributes, noTrailingSlash } from "./utils.js"; import { ALL_HASHES } from "$test/test-utils.js"; import { resetRoutingOptions, setRoutingOptions } from "./kernel/options.js"; import type { ActiveStateAriaAttributes, ExtendedRoutingOptions, Hash } from "./types.js"; @@ -73,3 +73,22 @@ describe("expandAriaAttributes", () => { expect(result).toEqual(expected); }); }); + +describe("noTrailingSlash", () => { + test.each<{ + input: string; + expected: string; + }>([ + { input: '/path/', expected: '/path' }, + { input: '/path/to/resource/', expected: '/path/to/resource' }, + { input: '/path', expected: '/path' }, + { input: '/', expected: '/' }, + { input: '', expected: '' }, + ])("Should convert $input to $expected .", ({ input, expected }) => { + // Act. + const result = noTrailingSlash(input); + + // Assert. + expect(result).toBe(expected); + }); +}); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ffaec02..6668797 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -64,3 +64,7 @@ export function expandAriaAttributes(aria: ActiveStateAriaAttributes | undefined } return result; } + +export function noTrailingSlash(path: string) { + return path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path; +}