Skip to content

Commit

Permalink
feat(lib): dirname
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed Dec 9, 2022
1 parent 3ece912 commit 18d449a
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 2 deletions.
11 changes: 9 additions & 2 deletions src/internal/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
* @module pathe/internal/constants
*/

/**
* Dot character.
*
* @const {string} DOT
*/
const DOT: string = '.'

/**
* Drive path regex.
*
Expand All @@ -25,6 +32,6 @@ const DRIVE_PATH_REGEX: RegExp = /^(?<drive>(?<letter>[a-z]):)/i
* @const {RegExp} UNC_PATH_REGEX
*/
const UNC_PATH_REGEX: RegExp =
/^[/\\]{2,}(?<host>[^/\\]+)[/\\]+(?<share>[^/\\]+)(?:[/\\]+(?<dir>[^\n/\\]+))?(?:[/\\]+(?<file>[^\n/\\]+))?[/\\]*(?=\n?$)/
/^[/\\]{2,}(?<host>[^/\\]+)(?:[/\\]+(?<share>[^/\\]+))?(?:[/\\]+(?<dir>[^\n/\\]+))?(?:[/\\]+(?<file>[^\n/\\]+))?[/\\]*(?:(?=\b$)|.+(?=\n?$))/

export { DRIVE_PATH_REGEX, UNC_PATH_REGEX }
export { DOT, DRIVE_PATH_REGEX, UNC_PATH_REGEX }
90 changes: 90 additions & 0 deletions src/lib/__tests__/dirname.spec.ts
Original file line number Diff line number Diff line change
@@ -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<typeof testSubject>[] = [
[''],
['//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<typeof testSubject>[] = [
[''],
['/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)))
})
})
})
})
154 changes: 154 additions & 0 deletions src/lib/dirname.ts
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

0 comments on commit 18d449a

Please sign in to comment.