|
|
@@ -1,61 +1,110 @@
|
|
|
-function matchSlashes(path: string): RegExpMatchArray | null {
|
|
|
- // https://regex101.com/r/FzHxQ9/1
|
|
|
- return path.match(/^(?=\/|[^\n])((\/+)?([^\n]+?))(\/+)?$/);
|
|
|
+interface PathParts {
|
|
|
+ readonly headingSlashes: string;
|
|
|
+ readonly content: string;
|
|
|
+ readonly trailingSlashes: string;
|
|
|
+ readonly hasHeadingSlash: boolean;
|
|
|
+ readonly hasTrailingSlash: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+function parsePath(path: string): PathParts | null {
|
|
|
+ if (!path || path === '') return null;
|
|
|
+
|
|
|
+ // Special case for root path
|
|
|
+ if (path === '/') {
|
|
|
+ return {
|
|
|
+ headingSlashes: '/',
|
|
|
+ content: '',
|
|
|
+ trailingSlashes: '',
|
|
|
+ hasHeadingSlash: true,
|
|
|
+ hasTrailingSlash: true,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ let startIndex = 0;
|
|
|
+ let endIndex = path.length;
|
|
|
+
|
|
|
+ // Find leading slashes
|
|
|
+ while (startIndex < path.length && path[startIndex] === '/') {
|
|
|
+ startIndex++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Find trailing slashes
|
|
|
+ while (endIndex > startIndex && path[endIndex - 1] === '/') {
|
|
|
+ endIndex--;
|
|
|
+ }
|
|
|
+
|
|
|
+ const headingSlashes = path.substring(0, startIndex);
|
|
|
+ const content = path.substring(startIndex, endIndex);
|
|
|
+ const trailingSlashes = path.substring(endIndex);
|
|
|
+
|
|
|
+ return {
|
|
|
+ headingSlashes,
|
|
|
+ content,
|
|
|
+ trailingSlashes,
|
|
|
+ hasHeadingSlash: headingSlashes.length > 0,
|
|
|
+ hasTrailingSlash: trailingSlashes.length > 0,
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
export function hasHeadingSlash(path: string): boolean {
|
|
|
if (path === '/') return true;
|
|
|
|
|
|
- const match = matchSlashes(path);
|
|
|
- return (match?.[2] != null);
|
|
|
+ const parts = parsePath(path);
|
|
|
+ return parts?.hasHeadingSlash ?? false;
|
|
|
}
|
|
|
|
|
|
export function hasTrailingSlash(path: string): boolean {
|
|
|
if (path === '/') return true;
|
|
|
|
|
|
- const match = matchSlashes(path);
|
|
|
- return (match?.[4] != null);
|
|
|
+ const parts = parsePath(path);
|
|
|
+ return parts?.hasTrailingSlash ?? false;
|
|
|
}
|
|
|
|
|
|
export function addHeadingSlash(path: string): string {
|
|
|
- if (path === '/') {
|
|
|
- return path;
|
|
|
- }
|
|
|
+ if (path === '/') return path;
|
|
|
+ if (path === '') return '/';
|
|
|
|
|
|
- if (!hasHeadingSlash(path)) {
|
|
|
+ const parts = parsePath(path);
|
|
|
+ if (!parts?.hasHeadingSlash) {
|
|
|
return `/${path}`;
|
|
|
}
|
|
|
return path;
|
|
|
}
|
|
|
|
|
|
export function addTrailingSlash(path: string): string {
|
|
|
- if (path === '/') {
|
|
|
- return path;
|
|
|
- }
|
|
|
+ if (path === '/') return path;
|
|
|
+ if (path === '') return '/';
|
|
|
|
|
|
- if (!hasTrailingSlash(path)) {
|
|
|
+ const parts = parsePath(path);
|
|
|
+ if (!parts?.hasTrailingSlash) {
|
|
|
return `${path}/`;
|
|
|
}
|
|
|
return path;
|
|
|
}
|
|
|
|
|
|
export function removeHeadingSlash(path: string): string {
|
|
|
- if (path === '/') {
|
|
|
- return path;
|
|
|
- }
|
|
|
+ if (path === '/') return path;
|
|
|
+ if (path === '') return path;
|
|
|
|
|
|
- return hasHeadingSlash(path)
|
|
|
- ? path.substring(1)
|
|
|
- : path;
|
|
|
+ const parts = parsePath(path);
|
|
|
+ if (!parts?.hasHeadingSlash) return path;
|
|
|
+
|
|
|
+ // Special case for '//' -> '/'
|
|
|
+ if (path === '//') return '/';
|
|
|
+
|
|
|
+ // Remove heading slashes and return content + trailing slashes
|
|
|
+ return parts.content + parts.trailingSlashes;
|
|
|
}
|
|
|
|
|
|
export function removeTrailingSlash(path: string): string {
|
|
|
- if (path === '/') {
|
|
|
- return path;
|
|
|
- }
|
|
|
+ if (path === '/') return path;
|
|
|
+ if (path === '') return path;
|
|
|
+
|
|
|
+ const parts = parsePath(path);
|
|
|
+ if (parts == null) return path;
|
|
|
|
|
|
- const match = matchSlashes(path);
|
|
|
- return match != null ? match[1] : path;
|
|
|
+ // Return heading slashes + content (without trailing slashes)
|
|
|
+ return parts.headingSlashes + parts.content;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -66,11 +115,11 @@ export function normalizePath(path: string): string {
|
|
|
return '/';
|
|
|
}
|
|
|
|
|
|
- const match = matchSlashes(path);
|
|
|
- if (match == null) {
|
|
|
+ const parts = parsePath(path);
|
|
|
+ if (parts == null) {
|
|
|
return '/';
|
|
|
}
|
|
|
- return `/${match[3]}`;
|
|
|
+ return `/${parts.content}`;
|
|
|
}
|
|
|
|
|
|
|