From 18d449af8a5ece7f3f67012d9e93ac6d2bd9a328 Mon Sep 17 00:00:00 2001 From: Lexus Drumgold Date: Thu, 8 Dec 2022 21:05:39 -0500 Subject: [PATCH] feat(lib): `dirname` Signed-off-by: Lexus Drumgold --- src/internal/constants.ts | 11 ++- src/lib/__tests__/dirname.spec.ts | 90 +++++++++++++++++ src/lib/dirname.ts | 154 ++++++++++++++++++++++++++++++ src/lib/index.ts | 1 + 4 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 src/lib/__tests__/dirname.spec.ts create mode 100644 src/lib/dirname.ts diff --git a/src/internal/constants.ts b/src/internal/constants.ts index d759f189..225e642f 100644 --- a/src/internal/constants.ts +++ b/src/internal/constants.ts @@ -3,6 +3,13 @@ * @module pathe/internal/constants */ +/** + * Dot character. + * + * @const {string} DOT + */ +const DOT: string = '.' + /** * Drive path regex. * @@ -25,6 +32,6 @@ const DRIVE_PATH_REGEX: RegExp = /^(?(?[a-z]):)/i * @const {RegExp} UNC_PATH_REGEX */ const UNC_PATH_REGEX: RegExp = - /^[/\\]{2,}(?[^/\\]+)[/\\]+(?[^/\\]+)(?:[/\\]+(?[^\n/\\]+))?(?:[/\\]+(?[^\n/\\]+))?[/\\]*(?=\n?$)/ + /^[/\\]{2,}(?[^/\\]+)(?:[/\\]+(?[^/\\]+))?(?:[/\\]+(?[^\n/\\]+))?(?:[/\\]+(?[^\n/\\]+))?[/\\]*(?:(?=\b$)|.+(?=\n?$))/ -export { DRIVE_PATH_REGEX, UNC_PATH_REGEX } +export { DOT, DRIVE_PATH_REGEX, UNC_PATH_REGEX } diff --git a/src/lib/__tests__/dirname.spec.ts b/src/lib/__tests__/dirname.spec.ts new file mode 100644 index 00000000..7a5aad1b --- /dev/null +++ b/src/lib/__tests__/dirname.spec.ts @@ -0,0 +1,90 @@ +/** + * @file Unit Tests - dirname + * @module pathe/lib/tests/unit/dirname + * @see https://github.com/nodejs/node/blob/main/test/parallel/test-path-dirname.js + */ + +import sep from '#src/lib/sep' +import { posix, win32 } from 'node:path' +import testSubject from '../dirname' + +describe('unit:lib/dirname', () => { + /** + * Converts Windows-style path separators (`\`) to POSIX (`/`). + * + * @param {string} path - Path to normalize + * @return {string} `path` normalized + */ + const ensurePosix = (path: string): string => path.replace(/\\/g, sep) + + it('should return directory name of path', () => { + // Arrange + const cases: Parameters[] = [ + [''], + ['//a'], + ['/a'], + ['/a/b'], + ['/a/b/'], + ['a'], + ['foo'], + [posix.sep], + [posix.sep.repeat(4)] + ] + + // Act + Expect + cases.forEach(([path]) => { + expect(testSubject(path)).to.equal(posix.dirname(path)) + }) + }) + + describe('windows', () => { + it('should return directory name of path', () => { + // Arrange + const cases: Parameters[] = [ + [''], + ['/a'], + ['/a/b'], + ['/a/b/'], + ['\\\\unc\\share'], + ['\\\\unc\\share\\foo'], + ['\\\\unc\\share\\foo\\'], + ['\\\\unc\\share\\foo\\bar'], + ['\\\\unc\\share\\foo\\bar\\'], + ['\\\\unc\\share\\foo\\bar\\baz'], + ['\\foo bar\\baz'], + ['\\foo'], + ['\\foo\\'], + ['\\foo\\bar'], + ['\\foo\\bar\\'], + ['\\foo\\bar\\baz'], + ['a'], + ['c:'], + ['c:\\'], + ['c:\\foo bar\\baz'], + ['c:\\foo'], + ['c:\\foo\\'], + ['c:\\foo\\bar'], + ['c:\\foo\\bar\\'], + ['c:\\foo\\bar\\baz'], + ['c:foo bar\\baz'], + ['c:foo'], + ['c:foo\\'], + ['c:foo\\bar'], + ['c:foo\\bar\\'], + ['c:foo\\bar\\baz'], + ['dir\\file:stream'], + ['file:stream'], + ['foo'], + [posix.sep], + [posix.sep.repeat(4)], + [win32.sep], + [win32.sep.repeat(2)] + ] + + // Act + Expect + cases.forEach(([path]) => { + expect(testSubject(path)).to.equal(ensurePosix(win32.dirname(path))) + }) + }) + }) +}) diff --git a/src/lib/dirname.ts b/src/lib/dirname.ts new file mode 100644 index 00000000..4c6986ef --- /dev/null +++ b/src/lib/dirname.ts @@ -0,0 +1,154 @@ +/** + * @file dirname + * @module pathe/lib/dirname + */ + +import { DOT } from '#src/internal/constants' +import ensurePosix from '#src/internal/ensure-posix' +import isDrivePath from '#src/internal/is-drive-path' +import isSep from '#src/internal/is-sep' +import isUncPath from '#src/internal/is-unc-path' +import validateString from '#src/internal/validate-string' +import sep from './sep' + +/** + * Returns the directory name of a path, similar to the Unix `dirname` command. + * + * Trailing [directory separators][1] are ignored. + * + * [1]: https://nodejs.org/api/path.html#pathsep + * + * @param {string} path - Path to evaluate + * @return {string} Directory name of `path` + * @throws {TypeError} If `path` is not a string + */ +const dirname = (path: string): string => { + validateString(path, 'path') + + // exit early if path is empty string + if (path.length === 0) return DOT + + // ensure path meets posix standards + path = ensurePosix(path) + + // exit early if path length equals 1 + if (path.length === 1) return isSep(path) ? path : DOT + + /** + * UNC path check. + * + * @const {boolean} unc + */ + const unc: boolean = isUncPath(path) + + /** + * Leading path separator check. + * + * @const {boolean} root + */ + const root: boolean = isSep(path[0]) + + /** + * Start index of directory name. + * + * @var {number} start + */ + let start: number = root ? 1 : 0 + + /** + * End index of directory name. + * + * @var {number} end + */ + let end: number = -1 + + /** + * Directory separator match check. + * + * @var {boolean} sep_match + */ + let sep_match: boolean = true + + // adjust start index if path starts with drive letter + if (isDrivePath(path)) { + start = path.length > 2 && isSep(path.charAt(2)) ? 3 : 2 + } + + // adjust start index if path is unc path + if (unc) { + // reset start index of directory name + start = 1 + + // match unc roots + if (isSep(path.charAt(1))) { + /** + * Current position in {@linkcode path}. + * + * @var {number} j + */ + let j: number = 2 + + /** + * Last visited position in {@linkcode path}. + * + * @var {number} last + */ + let last: number = j + + // match 1 or more non-directory separators + while (j < path.length && !isSep(path.charAt(j))) j++ + + if (j < path.length && j !== last) { + // set index of non-directory separator match + last = j + + // match 1 or more directory separators + while (j < path.length && isSep(path.charAt(j))) j++ + + if (j < path.length && j !== last) { + // set index of separator match + last = j + + // match 1 or more non-directory separators + while (j < path.length && !isSep(path.charAt(j))) j++ + + // matched unc root + if (j === path.length) return path + + // matched unc root with leftovers + // offset by 1 to include the separator after the root so that it is + // treated as a "normal root" on top of a unc root + if (j !== last) end = start = j + 1 + } + } + } + } + + // get end index of directory name + for (let i = path.length - 1; i >= start; --i) { + if (isSep(path.charAt(i))) { + // set end index of directory name + if (!sep_match) { + end = i + break + } + } else { + // non-directory separator was encountered + sep_match = false + } + } + + return end === -1 + ? root && !unc + ? sep + : isDrivePath(path) + ? path.length <= 3 + ? path + : path.slice(0, start) + : DOT + : root && end === 1 + ? sep + sep + : path.slice(0, end) +} + +export default dirname diff --git a/src/lib/index.ts b/src/lib/index.ts index b538fda6..8fd81214 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -5,4 +5,5 @@ export { default as basename } from './basename' export { default as delimiter } from './delimiter' +export { default as dirname } from './dirname' export { default as sep } from './sep'