Skip to content

Commit

Permalink
feat(lib): parse
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed Dec 11, 2022
1 parent de150ee commit 98c393a
Show file tree
Hide file tree
Showing 13 changed files with 237 additions and 26 deletions.
13 changes: 13 additions & 0 deletions src/interfaces/__tests__/parsed-path.spec-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @file Unit Tests - ParsedPath
* @module pathe/interfaces/tests/unit-d/ParsedPath
*/

import type TestSubject from '../parsed-path'
import type PathObject from '../path-object'

describe('unit-d:interfaces/ParsedPath', () => {
it('should extend Required<PathObject>', () => {
expectTypeOf<TestSubject>().toMatchTypeOf<Required<PathObject>>()
})
})
6 changes: 5 additions & 1 deletion src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@
* @module pathe/interfaces
*/

export type { default as PathObject } from './path-object'
export type { default as ParsedPath } from './parsed-path'
export type {
default as FormatInputPathObject,
default as PathObject
} from './path-object'
23 changes: 23 additions & 0 deletions src/interfaces/parsed-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @file Interfaces - ParsedPath
* @module pathe/interfaces/ParsedPath
*/

import type PathObject from './path-object'

/**
* Object representing significant elements of a path.
*
* This object can be generated by [`parse`][1] or consumed by [`format`][2].
*
* [1]: {@link ../lib/parse.ts}
* [2]: {@link ../lib/format.ts}
* [3]: {@link ./path-object.ts}
*
* @see [`PathObject`][3]
*
* @extends {Required<PathObject>}
*/
interface ParsedPath extends Required<PathObject> {}

export type { ParsedPath as default }
9 changes: 8 additions & 1 deletion src/internal/__tests__/is-unc-path.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ describe('unit:internal/isUncPath', () => {
})

it('should return true if path is UNC path', () => {
expect(testSubject('\\\\Server2\\Share\\Test\\Foo.txt')).to.be.true
expect(testSubject('\\\\\\Server2\\Share\\Test\\Foo.txt', false)).to.be.true
})

describe('exact', () => {
it('should return true if path starts with exactly two separators', () => {
expect(testSubject('//host/share/file.ext', true)).to.be.true
expect(testSubject('\\\\host\\share\\file.ext', true)).to.be.true
})
})
})
4 changes: 2 additions & 2 deletions src/internal/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const DOT: string = '.'
*
* @const {RegExp} DRIVE_PATH_REGEX
*/
const DRIVE_PATH_REGEX: RegExp = /^(?<drive>(?<letter>[a-z]):)(?:\/|\\{2})?/i
const DRIVE_PATH_REGEX: RegExp = /^(?<drive>(?<letter>[a-z]):(?:\/|\\{2})?)/i

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

export { DOT, DRIVE_PATH_REGEX, UNC_PATH_REGEX }
6 changes: 4 additions & 2 deletions src/internal/is-unc-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import validateString from './validate-string'
* @see https://learn.microsoft.com/dotnet/standard/io/file-path-formats#unc-paths
*
* @param {string} path - Path to evaluate
* @param {boolean} [exact=true] - Check for exactly two leading separators
* @return {boolean} `true` if path is UNC path
*/
const isUncPath = (path: string): boolean => {
return validateString(path, 'path') && UNC_PATH_REGEX.test(path)
const isUncPath = (path: string, exact: boolean = true): boolean => {
exact = exact ? /^[/\\]{2}[^/\\]/.test(path) : true
return validateString(path, 'path') && UNC_PATH_REGEX.test(path) && exact
}

export default isUncPath
1 change: 1 addition & 0 deletions src/lib/__tests__/dirname.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('unit:lib/dirname', () => {
['\\foo\\bar\\baz'],
['a'],
['c:'],
['c:.'],
['c:\\'],
['c:\\foo bar\\baz'],
['c:\\foo'],
Expand Down
88 changes: 88 additions & 0 deletions src/lib/__tests__/parse.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @file Unit Tests - parse
* @module pathe/lib/tests/unit/parse
* @see https://github.com/nodejs/node/blob/main/test/parallel/test-path-parse-format.js
* @see https://github.com/nodejs/node/issues/18655
*/

import type { ParsedPath } from '#src/interfaces'
import sep from '#src/lib/sep'
import { posix, win32 } from 'node:path'
import testSubject from '../parse'

describe('unit:lib/parse', () => {
it('should return parsed path object', () => {
// Arrange
const cases: Parameters<typeof testSubject>[] = [
[''],
['/.'],
['/.foo'],
['/.foo.bar'],
['/foo'],
['/foo.'],
['/foo.bar'],
['/foo///'],
['/foo///bar.baz'],
['/foo/bar.baz'],
['/home/user/a dir//another&file.'],
['/home/user/a dir/another file.zip'],
['/home/user/a$$$dir//another file.zip'],
['/home/user/dir/file.txt'],
['/home/user/dir/file.txt'],
['user/dir/another file.zip'],
[posix.sep],
[posix.sep.repeat(2)],
[posix.sep.repeat(3)]
]

// Act + Expect
cases.forEach(([path]) => {
expect(testSubject(path)).to.deep.equal(posix.parse(path))
})
})

describe('windows', () => {
/**
* Converts Windows-style path separators (`\`) to POSIX (`/`).
*
* @param {ParsedPath} parsed - Parsed path object to normalize
* @return {string} `parsed` with values normalized
*/
const ensurePosix = (parsed: ParsedPath): ParsedPath => {
for (const [key, value] of Object.entries<string>(parsed)) {
if (!value) continue
parsed[key] = value.replace(/\\/g, sep)
}

return parsed
}

it('should return parsed path object', () => {
// Arrange
const cases: Parameters<typeof testSubject>[] = [
[''],
['.\\file'],
['C:'],
['C:.'],
['C:..'],
['C:\\'],
['C:\\abc'],
['C:\\another_path\\DIR\\1\\2\\33\\\\index'],
['C:\\path\\dir\\index.html'],
['C:abc'],
['\\\\?\\UNC\\server\\share'],
['\\\\server two\\shared folder\\file path.zip'],
['\\\\server\\share\\file_path'],
['\\\\user\\admin$\\system32'],
['\\foo\\C:'],
['another_path\\DIR with spaces\\1\\2\\33\\index'],
[win32.sep]
]

// Act + Expect
cases.forEach(([path]) => {
expect(testSubject(path)).to.deep.equal(ensurePosix(win32.parse(path)))
})
})
})
})
2 changes: 1 addition & 1 deletion src/lib/basename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const basename = (path: string, suffix?: string): string => {
/**
* Character at {@linkcode i} in {@linkcode path}.
*
* @const {string} code
* @const {string} char
*/
const char: string = path.charAt(i)

Expand Down
42 changes: 24 additions & 18 deletions src/lib/dirname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ 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 isAbsolute from './is-absolute'
import sep from './sep'

/**
Expand All @@ -35,18 +35,18 @@ const dirname = (path: string): string => {
if (path.length === 1) return isSep(path) ? path : DOT

/**
* UNC path check.
* Drive path check.
*
* @const {boolean} unc
* @const {boolean} drive
*/
const unc: boolean = isUncPath(path)
const drive: boolean = isDrivePath(path)

/**
* Leading path separator check.
*
* @const {boolean} root
*/
const root: boolean = isSep(path[0])
const root: boolean = isSep(path.charAt(0))

/**
* Start index of directory name.
Expand All @@ -69,17 +69,22 @@ const dirname = (path: string): string => {
*/
let sep_match: boolean = true

/**
* UNC path check.
*
* @var {boolean} unc
*/
let unc: boolean = false

// adjust start index if path starts with drive letter
if (isDrivePath(path)) {
start = path.length > 2 && isSep(path.charAt(2)) ? 3 : 2
}
if (drive) start = path.length > 2 && isAbsolute(path) ? 3 : 2

// adjust start index if path is unc path
if (unc) {
// adjust start index if path is absolute
if (isSep(path.charAt(0))) {
// reset start index of directory name
start = 1

// match unc roots
// adjust start and end indices if path is unc path
if (isSep(path.charAt(1))) {
/**
* Current position in {@linkcode path}.
Expand All @@ -99,14 +104,14 @@ const dirname = (path: string): string => {
while (j < path.length && !isSep(path.charAt(j))) j++

if (j < path.length && j !== last) {
// set index of non-directory separator match
// set last visited position to index of directory separator
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
// set last visited position to index of non-directory separator
last = j

// match 1 or more non-directory separators
Expand All @@ -118,7 +123,10 @@ const dirname = (path: string): string => {
// 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
if (j !== last) {
unc = true
end = start = j + 1
}
}
}
}
Expand All @@ -141,10 +149,8 @@ const dirname = (path: string): string => {
return end === -1
? root && !unc
? sep
: isDrivePath(path)
? path.length <= 3
? path
: path.slice(0, start)
: drive
? path.slice(0, start)
: DOT
: root && end === 1
? sep + sep
Expand Down
2 changes: 1 addition & 1 deletion src/lib/extname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { Ext } from '#src/types'
import type { EmptyString } from '@flex-development/tutils'

/**
* Returns the extension of a path, from the last occurrence of the `.` (period)
* Returns the extension of a path, from the last occurrence of the `.` (dot)
* character to end of string in the last portion of the path.
*
* If there is no `.` in the last portion of the path, or if there are no `.`
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export { default as format } from './format'
export { default as isAbsolute } from './is-absolute'
export { default as join } from './join'
export { default as normalize } from './normalize'
export { default as parse } from './parse'
export { default as sep } from './sep'
66 changes: 66 additions & 0 deletions src/lib/parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* @file parse
* @module pathe/lib/parse
*/

import type { ParsedPath } from '#src/interfaces'
import { DRIVE_PATH_REGEX, UNC_PATH_REGEX } from '#src/internal/constants'
import ensurePosix from '#src/internal/ensure-posix'
import isDrivePath from '#src/internal/is-drive-path'
import isUncPath from '#src/internal/is-unc-path'
import validateString from '#src/internal/validate-string'
import removeExt from '#src/utils/remove-ext'
import basename from './basename'
import dirname from './dirname'
import extname from './extname'
import isAbsolute from './is-absolute'
import sep from './sep'

/**
* Returns an object representing the given `path`.
*
* Trailing directory [separators][1] are ignored.
*
* **Note**: Unlike in Node.js, `pathe.parse(path).dir === pathe.dirname(path)`
* when `path` is a non-empty string. See [`nodejs/node#18655`][3] for details.
*
* [1]: {@link ./sep.ts}
* [2]: {@link ./dirname.ts}
* [3]: https://github.com/nodejs/node/issues/18655
*
* @param {string} path - Path to evaluate
* @return {ParsedPath} Object representing significant elements of `path`
* @throws {TypeError} If `path` is not a string
*/
const parse = (path: string): ParsedPath => {
validateString(path, 'path')

// ensure path meets posix standards
path = ensurePosix(path)

/**
* Parsed path object.
*
* @const {ParsedPath} ret
*/
const ret: ParsedPath = { base: '', dir: '', ext: '', name: '', root: '' }

// exit early if path is empty string
if (path.length === 0) return ret

ret.base = basename(path)
ret.dir = dirname(path)
ret.ext = extname(path)
ret.name = removeExt(ret.base, ret.ext)
ret.root = isUncPath(path)
? UNC_PATH_REGEX.exec(path)![1]!
: isDrivePath(path)
? DRIVE_PATH_REGEX.exec(path)![1]!
: isAbsolute(path)
? sep
: ''

return ret
}

export default parse

0 comments on commit 98c393a

Please sign in to comment.