// Copyright 2023 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. export type Location = Readonly<{ pathname?: string; searchParams?: URLSearchParams; }>; export type Segments = Readonly; // Converts a list of segments to a path const segmentsToPath = (segments: Segments): string => segments.map((part) => encodeURIComponent(part)).join("/"); // Converts a path to a list of segments const pathToSegments = (path: string): Segments => path.split("/").map(decodeURIComponent); type ProfileRoute = Readonly<{ type: "profile" }>; type SessionOverviewRoute = Readonly<{ type: "sessions-overview" }>; type SessionDetailRoute = Readonly<{ type: "session"; id: string }>; type OAuth2ClientRoute = Readonly<{ type: "client"; id: string }>; type BrowserSessionRoute = Readonly<{ type: "browser-session"; id: string }>; type BrowserSessionListRoute = Readonly<{ type: "browser-session-list" }>; type VerifyEmailRoute = Readonly<{ type: "verify-email"; id: string }>; type UnknownRoute = Readonly<{ type: "unknown"; segments: Segments }>; export type Route = | SessionOverviewRoute | SessionDetailRoute | ProfileRoute | OAuth2ClientRoute | BrowserSessionRoute | BrowserSessionListRoute | VerifyEmailRoute | UnknownRoute; // Converts a route to a path export const routeToPath = (route: Route): string => segmentsToPath(routeToSegments(route)); // Converts a path to a route export const pathToRoute = (path: string): Route => segmentsToRoute(pathToSegments(path)); // Converts a route to a list of segments export const routeToSegments = (route: Route): Segments => { switch (route.type) { case "profile": return []; case "sessions-overview": return ["sessions-overview"]; case "session": return ["session", route.id]; case "verify-email": return ["emails", route.id, "verify"]; case "client": return ["clients", route.id]; case "browser-session-list": return ["browser-sessions"]; case "browser-session": return ["browser-sessions", route.id]; case "unknown": return route.segments; } }; const P = Symbol(); type PatternItem = string | typeof P; // Returns true if the segments match the pattern, where P is a parameter const segmentMatches = ( segments: Segments, ...pattern: PatternItem[] ): boolean => { // Quick check to see if the lengths match if (segments.length !== pattern.length) return false; // Check each segment for (let i = 0; i < segments.length; i++) { // If the pattern is P, then it's a parameter and we can skip it if (pattern[i] === P) continue; // Otherwise, check that the segment matches the pattern if (segments[i] !== pattern[i]) return false; } return true; }; // Converts a list of segments to a route export const segmentsToRoute = (segments: Segments): Route => { const matches = (...pattern: PatternItem[]): boolean => segmentMatches(segments, ...pattern); // Special case for the home page if (matches() || matches("")) { return { type: "profile" }; } if (matches("sessions-overview")) { return { type: "sessions-overview" }; } if (matches("browser-sessions")) { return { type: "browser-session-list" }; } if (matches("emails", P, "verify")) { return { type: "verify-email", id: segments[1] }; } if (matches("clients", P)) { return { type: "client", id: segments[1] }; } if (matches("browser-sessions", P)) { return { type: "browser-session", id: segments[1] }; } if (matches("session", P)) { return { type: "session", id: segments[1] }; } return { type: "unknown", segments }; };