Yuki Takei 10 месяцев назад
Родитель
Сommit
98142aadf6
2 измененных файлов с 79 добавлено и 30 удалено
  1. 1 1
      packages/core/src/utils/path-utils.spec.ts
  2. 78 29
      packages/core/src/utils/path-utils.ts

+ 1 - 1
packages/core/src/utils/path-utils.spec.ts

@@ -120,7 +120,7 @@ describe('page-utils', () => {
       ${'/path1/path2'}     | ${'path1/path2'}
       ${'path1/path2/'}     | ${'path1/path2/'}
       ${'/path1/path2/'}    | ${'path1/path2/'}
-      ${'//path1/path2//'}  | ${'/path1/path2//'}
+      ${'//path1/path2//'}  | ${'path1/path2//'}
       ${'https://example.com'} | ${'https://example.com'}
       ${'https://example.com/'} | ${'https://example.com/'}
       ${'//'}               | ${'/'}                  // from former specific test

+ 78 - 29
packages/core/src/utils/path-utils.ts

@@ -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}`;
 }