Skip to content

Commit

Permalink
feat(lib): normalize
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 1cf683e commit d937719
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 2 deletions.
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]):)/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]):)/i
* @const {RegExp} UNC_PATH_REGEX
*/
const UNC_PATH_REGEX: RegExp =
/^[/\\]{2,}(?<host>[^/\\]+)(?:[/\\]+(?<share>[^/\\]+))?(?:[/\\]+(?<dir>[^\n/\\]+))?(?:[/\\]+(?<file>[^\n/\\]+))?[/\\]*(?:(?=\b$)|.+(?=\n?$))/
/^(?<volume>(?<root>[/\\]{2,})(?<host>[^/\\]+)[/\\]+(?<share>[^/\\]+))[/\\]*(?:(?<dir>.+?)[/\\]*(?<file>[^/\\]+\..[^/\\]+)?[/\\]*(?=[/\\]*\n?$))?/

export { DOT, DRIVE_PATH_REGEX, UNC_PATH_REGEX }
110 changes: 110 additions & 0 deletions src/lib/__tests__/normalize.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* @file Unit Tests - normalize
* @module pathe/lib/tests/unit/normalize
* @see https://github.com/nodejs/node/blob/main/test/parallel/test-path-normalize.js
*/

import sep from '#src/lib/sep'
import { posix, win32 } from 'node:path'
import testSubject from '../normalize'

describe('unit:lib/normalize', () => {
it('should return normalized path', () => {
// Arrange
const cases: Parameters<typeof testSubject>[] = [
[''],
[' '],
['../.../.././.../../../bar'],
['../.../../foobar/../../../bar/../../baz'],
['../../../foo/../../../bar'],
['../../../foo/../../../bar/../../'],
['../foo../../../bar'],
['../foobar/barfoo/foo/../../../bar/../../'],
['./'],
['./../'],
['./a/..'],
['./a/../'],
['./fixtures///b/../b/c.js'],
['///..//./foo/.//bar'],
['/a/..'],
['/a/b/c/../../../x/y/z'],
['/foo/../../../bar'],
['/foo/bar//baz/asdf/quux/..'],
['a//b//.'],
['a//b//../b'],
['a//b//./c'],
['bar/foo..'],
['bar/foo../'],
['bar/foo../..'],
['bar/foo../../'],
['bar/foo../../baz'],
[import.meta.url],
[posix.sep]
]

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

describe('windows', () => {
/**
* 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 normalized path', () => {
// Arrange
const cases: Parameters<typeof testSubject>[] = [
[''],
[' '],
['../.../../foobar/../../../bar/../../baz'],
['../../../foo/../../../bar'],
['../../../foo/../../../bar/../../'],
['../foobar/barfoo/foo/../../../bar/../../'],
['..\\...\\..\\.\\...\\..\\..\\bar'],
['..\\foo..\\..\\..\\bar'],
['./fixtures///b/../b/c.js'],
['//foo//bar'],
['//foo/bar'],
['//server/share/dir/file.ext'],
['/a/b/c/../../../x/y/z'],
['/foo/../../../bar'],
['C:'],
['C:..\\..\\abc\\..\\def'],
['C:..\\abc'],
['C:\\'],
['C:\\.'],
['C:\\temp\\..'],
['\\\\.\\c:\\temp\\file\\..\\path'],
['\\\\.\\foo\\bar'],
['\\\\.\\host\\share\\dir\\file.txt\\'],
['\\\\C:\\foo\\bar'],
['\\\\server/share/file/../path'],
['\\\\server\\share\\file\\..\\path'],
['a//b//.'],
['a//b//../b'],
['a//b//./c'],
['bar\\foo..'],
['bar\\foo..\\'],
['bar\\foo..\\..'],
['bar\\foo..\\..\\'],
['bar\\foo..\\..\\baz'],
['file:stream'],
['foo\\'],
['foo\\bar\\baz'],
[import.meta.url],
[win32.sep]
]

// Act + Expect
cases.forEach(([path]) => {
expect(testSubject(path)).to.equal(ensurePosix(win32.normalize(path)))
})
})
})
})
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export { default as dirname } from './dirname'
export { default as extname } from './extname'
export { default as format } from './format'
export { default as isAbsolute } from './is-absolute'
export { default as normalize } from './normalize'
export { default as sep } from './sep'
143 changes: 143 additions & 0 deletions src/lib/normalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* @file normalize
* @module pathe/lib/normalize
*/

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 normalizeString from '#src/internal/normalize-string'
import validateString from '#src/internal/validate-string'
import isAbsolute from './is-absolute'
import sep from './sep'

/**
* Normalizes the given `path`, resolving `'..'` and `'.'` segments.
*
* When multiple, sequential path segment separation characters are found (e.g.
* `/` or `\`), they are replaced by a single instance of [`sep`][1]. Trailing
* separators are preserved.
*
* If the given `path` is a zero-length string, `'.'` is returned, representing
* the current working directory.
*
* [1]: {@link ../lib/sep.ts}
*
* @param {string} path - Path to normalize
* @return {string} Normalized `path`
* @throws {TypeError} If `path` is not a string
*/
const normalize = (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 is one character
if (path.length === 1) return path

/**
* Absolute path check.
*
* @const {boolean} absolute
*/
const absolute: boolean = isAbsolute(path)

/**
* Trailing separator check.
*
* @var {string} trail
*/
const trail: string = isSep(path.charAt(path.length - 1)) ? sep : ''

/**
* Drive letter, UNC path component, or empty string.
*
* @var {string} device
*/
let device: string = ''

/**
* Index to begin path normalization.
*
* @var {number} offset
*/
let offset: number = 0

// adjust normalization offset if path is drive path
if (isDrivePath(path)) device = path.slice(0, (offset = 2))

// try adjusting normalization offset if path is possible unc path
if (isSep(path.charAt(0)) && 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-path separators
while (j < path.length && !isSep(path.charAt(j))) j++

if (j < path.length && j !== last) {
/**
* Possible UNC path component.
*
* @const {string} host
*/
const host: string = path.slice(last, j)

/**
* Checks if {@linkcode host} is actually a `".."` segment.
*
* @const {boolean} dotdot
*/
const dotdot: boolean = host === DOT.repeat(2)

// set last visited position to end of host
last = j

// match 1 or more path separators
while (j < path.length && isSep(path.charAt(j))) j++

if (j < path.length && j !== last) {
// set last visited position
last = j

// match 1 or more non-path separators
while (j < path.length && !isSep(path.charAt(j))) j++

// matched unc root only => nothing left to process
if (j === path.length && !dotdot) {
return `${sep}${sep}${host}${sep}${path.slice(last)}${sep}`
}

// matched unc root with leftovers
if (j !== last && !dotdot) {
device = `${sep}${sep}${host}${sep}${path.slice(last, j)}`
offset = j
}
}
}
}

// normalize path
path = offset < path.length ? normalizeString(path.slice(offset)) : ''

return path.length === 0
? `${device}${absolute ? sep : DOT + trail}`
: `${device}${absolute ? sep : ''}${path}${trail}`
}

export default normalize

0 comments on commit d937719

Please sign in to comment.