Просмотр исходного кода

Merge pull request #9981 from weseek/imprv/path-utils-redos-safe

imprv: Regex for path-utils
Yuki Takei 10 месяцев назад
Родитель
Сommit
b7a588601c

+ 0 - 162
packages/core/src/utils/path-utils.js

@@ -1,162 +0,0 @@
-/**
- * @private
- *
- *
- * @param {string} path
- * @returns {RegExpMatchArray}
- * @memberof pathUtils
- */
-function matchSlashes(path) {
-  // https://regex101.com/r/Z21fEd/5
-  return path.match(/^((\/+)?(.+?))(\/+)?$/);
-}
-
-/**
- *
- * @param {string} path
- * @returns {boolean}
- * @memberof pathUtils
- */
-export function hasHeadingSlash(path) {
-  if (path === '') {
-    return false;
-  }
-  const match = matchSlashes(path);
-  return (match[2] != null);
-}
-
-/**
- *
- * @param {string} path
- * @returns {boolean}
- * @memberof pathUtils
- */
-export function hasTrailingSlash(path) {
-  if (path === '') {
-    return false;
-  }
-  const match = matchSlashes(path);
-  return (match[4] != null);
-}
-
-/**
- *
- * @param {string} path
- * @returns {string}
- * @memberof pathUtils
- */
-export function addHeadingSlash(path) {
-  if (path === '/') {
-    return path;
-  }
-
-  if (!hasHeadingSlash(path)) {
-    return `/${path}`;
-  }
-  return path;
-}
-
-/**
- *
- * @param {string} path
- * @returns {string}
- * @memberof pathUtils
- */
-export function addTrailingSlash(path) {
-  if (path === '/') {
-    return path;
-  }
-
-  if (!hasTrailingSlash(path)) {
-    return `${path}/`;
-  }
-  return path;
-}
-
-/**
- *
- * @param {string} path
- * @returns {string}
- * @memberof pathUtils
- */
-export function removeHeadingSlash(path) {
-  if (path === '/') {
-    return path;
-  }
-
-  return hasHeadingSlash(path)
-    ? path.substring(1)
-    : path;
-}
-
-/**
- *
- * @param {string} path
- * @returns {string}
- * @memberof pathUtils
- */
-export function removeTrailingSlash(path) {
-  if (path === '/') {
-    return path;
-  }
-
-  const match = matchSlashes(path);
-  return match[1];
-}
-
-/**
- * A short-hand method to add heading slash and remove trailing slash.
- *
- * @param {string} path
- * @returns {string}
- * @memberof pathUtils
- */
-export function normalizePath(path) {
-  if (path === '' || path === '/') {
-    return '/';
-  }
-
-  const match = matchSlashes(path);
-  if (match == null) {
-    return '/';
-  }
-  return `/${match[3]}`;
-}
-
-
-/**
- *
- * @param {string} path
- * @returns {string}
- * @memberof pathUtils
- */
-export function attachTitleHeader(path) {
-  return `# ${path}`;
-}
-
-/**
- * If the pagePath is top page path, eliminate the pageId from the url path.
- *
- * @param {string} path
- * @param {string} id
- * @returns {string}
- * @memberof pathUtils
- */
-export function returnPathForURL(path, id) {
-  if (path === '/') {
-    return path;
-  }
-
-  return addHeadingSlash(id);
-}
-
-/**
- * Get the parent path of the specified path.
- *
- * @param {string} path
- * @returns {string}
- * @memberof pathUtils
- */
-export function getParentPath(path) {
-  return normalizePath(path.split('/').slice(0, -1).join('/'));
-}

+ 162 - 11
packages/core/src/utils/path-utils.spec.ts

@@ -1,26 +1,177 @@
 import * as pathUtils from './path-utils';
 
-
 describe('page-utils', () => {
+
   describe('.normalizePath', () => {
-    test.concurrent('should return the root path with empty string', () => {
-      expect(pathUtils.normalizePath('')).toBe('/');
+    test.concurrent.each`
+      path                  | expected
+      ${'/'}                | ${'/'}
+      ${''}                 | ${'/'}
+      ${'path'}             | ${'/path'}
+      ${'/path'}            | ${'/path'}
+      ${'path/'}            | ${'/path'}
+      ${'/path/'}           | ${'/path'}
+      ${'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'}
+    `('should normalize \'$path\' to \'$expected\'', ({ path, expected }) => {
+      expect(pathUtils.normalizePath(path)).toBe(expected);
+    });
+  });
+
+  describe('.hasHeadingSlash', () => {
+    test.concurrent.each`
+      path                  | expected
+      ${'/'}                | ${true}
+      ${''}                 | ${false}
+      ${'path'}             | ${false}
+      ${'/path'}            | ${true}
+      ${'path/'}            | ${false}
+      ${'/path/'}           | ${true}
+      ${'path1/path2'}      | ${false}
+      ${'/path1/path2'}     | ${true}
+      ${'path1/path2/'}     | ${false}
+      ${'/path1/path2/'}    | ${true}
+      ${'//path1/path2//'}  | ${true}
+      ${'https://example.com'} | ${false}
+      ${'https://example.com/'} | ${false}
+    `('should return $expected when checking heading slash for \'$path\'', ({ path, expected }) => {
+      expect(pathUtils.hasHeadingSlash(path)).toBe(expected);
     });
+  });
 
-    test.concurrent('should return the root path as is', () => {
-      expect(pathUtils.normalizePath('/')).toBe('/');
+  describe('.hasTrailingSlash', () => {
+    test.concurrent.each`
+      path                  | expected
+      ${'/'}                | ${true}
+      ${''}                 | ${false}
+      ${'path'}             | ${false}
+      ${'/path'}            | ${false}
+      ${'path/'}            | ${true}
+      ${'/path/'}           | ${true}
+      ${'path1/path2'}      | ${false}
+      ${'/path1/path2'}     | ${false}
+      ${'path1/path2/'}     | ${true}
+      ${'/path1/path2/'}    | ${true}
+      ${'//path1/path2//'}  | ${true}
+      ${'https://example.com'} | ${false}
+      ${'https://example.com/'} | ${true}
+    `('should return $expected when checking trailing slash for \'$path\'', ({ path, expected }) => {
+      expect(pathUtils.hasTrailingSlash(path)).toBe(expected);
     });
+  });
 
-    test.concurrent('should add heading slash', () => {
-      expect(pathUtils.normalizePath('hoge/fuga')).toBe('/hoge/fuga');
+  describe('.addHeadingSlash', () => {
+    test.concurrent.each`
+      path                  | expected
+      ${'/'}                | ${'/'}
+      ${''}                 | ${'/'}
+      ${'path'}             | ${'/path'}
+      ${'/path'}            | ${'/path'}
+      ${'path/'}            | ${'/path/'}
+      ${'/path/'}           | ${'/path/'}
+      ${'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/'}
+    `('should add heading slash to \'$path\' resulting in \'$expected\'', ({ path, expected }) => {
+      expect(pathUtils.addHeadingSlash(path)).toBe(expected);
     });
+  });
 
-    test.concurrent('should remove trailing slash', () => {
-      expect(pathUtils.normalizePath('/hoge/fuga/')).toBe('/hoge/fuga');
+  describe('.addTrailingSlash', () => {
+    test.concurrent.each`
+      path                  | expected
+      ${'/'}                | ${'/'}
+      ${''}                 | ${'/'}
+      ${'path'}             | ${'path/'}
+      ${'/path'}            | ${'/path/'}
+      ${'path/'}            | ${'path/'}
+      ${'/path/'}           | ${'/path/'}
+      ${'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/'}
+    `('should add trailing slash to \'$path\' resulting in \'$expected\'', ({ path, expected }) => {
+      expect(pathUtils.addTrailingSlash(path)).toBe(expected);
     });
+  });
+
+  describe('.removeHeadingSlash', () => {
+    test.concurrent.each`
+      path                  | expected
+      ${'/'}                | ${'/'}
+      ${''}                 | ${''}
+      ${'path'}             | ${'path'}
+      ${'/path'}            | ${'path'}
+      ${'path/'}            | ${'path/'}
+      ${'/path/'}           | ${'path/'}
+      ${'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
+    `('should remove heading slash from \'$path\' resulting in \'$expected\'', ({ path, expected }) => {
+      expect(pathUtils.removeHeadingSlash(path)).toBe(expected);
+    });
+  });
+
+  describe('.removeTrailingSlash', () => {
+    test.concurrent.each`
+      path                  | expected
+      ${'/'}                | ${'/'}
+      ${''}                 | ${''}
+      ${'path'}             | ${'path'}
+      ${'/path'}            | ${'/path'}
+      ${'path/'}            | ${'path'}
+      ${'/path/'}           | ${'/path'}
+      ${'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'}
+    `('should remove trailing slash from \'$path\' resulting in \'$expected\'', ({ path, expected }) => {
+      expect(pathUtils.removeTrailingSlash(path)).toBe(expected);
+    });
+  });
 
-    test.concurrent('should remove unnecessary slashes', () => {
-      expect(pathUtils.normalizePath('//hoge/fuga//')).toBe('/hoge/fuga');
+  describe('.getParentPath', () => {
+    test.concurrent.each`
+      path                  | expected
+      ${'/'}                | ${'/'}
+      ${''}                 | ${'/'}
+      ${'path'}             | ${'/'}
+      ${'/path'}            | ${'/'}
+      ${'path/'}            | ${'/path'}
+      ${'/path/'}           | ${'/path'}
+      ${'path1/path2'}      | ${'/path1'}
+      ${'/path1/path2'}     | ${'/path1'}
+      ${'path1/path2/'}     | ${'/path1/path2'}
+      ${'/path1/path2/'}    | ${'/path1/path2'}
+      ${'//path1/path2//'}  | ${'/path1/path2'}
+      ${'https://example.com'} | ${'/https:'}
+      ${'https://example.com/'} | ${'/https://example.com'}
+      ${'/page'}            | ${'/'}                  // from former specific test
+      // Note: getParentPath('page') is covered by 'path' -> '/'
+      // Note: getParentPath('/path1/path2') is covered by '/path1/path2' -> '/path1'
+      // Note: getParentPath('/path1/path2/') is covered by '/path1/path2/' -> '/path1/path2'
+    `('should get parent path of \'$path\' as \'$expected\'', ({ path, expected }) => {
+      expect(pathUtils.getParentPath(path)).toBe(expected);
     });
   });
 });

+ 146 - 0
packages/core/src/utils/path-utils.ts

@@ -0,0 +1,146 @@
+interface PathParts {
+  readonly headingSlashes: string;
+  readonly content: string;
+  readonly trailingSlashes: string;
+  readonly hasHeadingSlash: boolean;
+  readonly hasTrailingSlash: boolean;
+}
+
+function parsePath(path: string): PathParts | null {
+  if (typeof path !== 'string' || !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 parts = parsePath(path);
+  return parts?.hasHeadingSlash ?? false;
+}
+
+export function hasTrailingSlash(path: string): boolean {
+  if (path === '/') return true;
+
+  const parts = parsePath(path);
+  return parts?.hasTrailingSlash ?? false;
+}
+
+export function addHeadingSlash(path: string): string {
+  if (path === '/') return path;
+  if (path === '') return '/';
+
+  const parts = parsePath(path);
+  if (!parts?.hasHeadingSlash) {
+    return `/${path}`;
+  }
+  return path;
+}
+
+export function addTrailingSlash(path: string): string {
+  if (path === '/') return path;
+  if (path === '') return '/';
+
+  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;
+
+  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;
+
+  const parts = parsePath(path);
+  if (parts == null) return path;
+
+  // Return heading slashes + content (without trailing slashes)
+  return parts.headingSlashes + parts.content;
+}
+
+/**
+ * A short-hand method to add heading slash and remove trailing slash.
+ */
+export function normalizePath(path: string): string {
+  if (typeof path !== 'string' || path === '' || path === '/') {
+    return '/';
+  }
+
+  const parts = parsePath(path);
+  if (parts == null) {
+    return '/';
+  }
+  return `/${parts.content}`;
+}
+
+
+export function attachTitleHeader(path: string): string {
+  return `# ${path}`;
+}
+
+/**
+ * If the pagePath is top page path, eliminate the pageId from the url path.
+ */
+export function returnPathForURL(path: string, id: string): string {
+  if (path === '/') {
+    return path;
+  }
+
+  return addHeadingSlash(id);
+}
+
+/**
+ * Get the parent path of the specified path.
+ */
+export function getParentPath(path: string): string {
+  return normalizePath(path.split('/').slice(0, -1).join('/'));
+}