Skip to content

Commit

Permalink
feat(internal): normalizeString
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed Dec 10, 2022
1 parent e4b3ad8 commit 1cf683e
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 21 deletions.
2 changes: 2 additions & 0 deletions .dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pathe
pkgs
posix
preid
seglen
sepidx
syncer
vates
vsicons
Expand Down
12 changes: 0 additions & 12 deletions src/internal/__tests__/ensure-posix.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,6 @@ describe('unit:internal/ensurePosix', () => {
path = 'C:\\Windows\\system32;C:\\Windows;C:\\Program Files\\node\\'
})

it('should consolidate duplicate posix path segment separators', () => {
expect(testSubject(path)).to.not.include('//')
})

it('should convert windows path delimiters', () => {
expect(testSubject(path)).to.not.include(';')
})

it('should convert windows path segment separators', () => {
expect(testSubject(path)).to.not.include('\\')
})

it('should return path that meets posix standards', () => {
// Arrange
const segments: string[] = [
Expand Down
78 changes: 78 additions & 0 deletions src/internal/__tests__/normalize-string.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* @file Unit Tests - normalizeString
* @module pathe/internal/tests/unit/normalizeString
*/

import testSubject from '../normalize-string'

describe('unit:internal/normalizeString', () => {
it('should return normalized string', () => {
// Arrange
const cases: [...Parameters<typeof testSubject>, string][] = [
['', ''],
[' ', ' '],
['../.../.././.../../../bar', '../../bar'],
['../.../../foobar/../../../bar/../../baz', '../../../../baz'],
['../../../foo/../../../bar', '../../../../../bar'],
['../../../foo/../../../bar/../../', '../../../../../..'],
['../foo../../../bar', '../../bar'],
['../foobar/barfoo/foo/../../../bar/../../', '../..'],
['./fixtures///b/../b/c.js', 'fixtures/b/c.js'],
['///..//./foo/.//bar', 'foo/bar'],
['/a/b/c/../../../x/y/z', 'x/y/z'],
['/foo/../../../bar', 'bar'],
['a//b//.', 'a/b'],
['a//b//../b', 'a/b'],
['a//b//./c', 'a/b/c'],
['bar/foo..', 'bar/foo..'],
['bar/foo../..', 'bar'],
['bar/foo../../', 'bar'],
['bar/foo../../baz', 'bar/baz'],
['foo', 'foo'],
['foo/bar/foo/bar/foo/../../../bar/../../', 'foo']
]

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

describe('windows', () => {
it('should return normalized string', () => {
// Arrange
const cases: [...Parameters<typeof testSubject>, string][] = [
['', ''],
[' ', ' '],
['..\\...\\..\\.\\...\\..\\..\\bar', '../../bar'],
['..\\..\\..\\foo\\..\\..\\..\\bar', '../../../../../bar'],
['..\\..\\..\\foo\\..\\..\\..\\bar\\..\\..\\', '../../../../../..'],
['..\\foo..\\..\\..\\bar', '../../bar'],
['..\\foobar\\barfoo\\foo\\..\\..\\..\\bar\\..\\..\\', '../..'],
['.\\fixtures\\\\\\b\\..\\b\\c.js', 'fixtures/b/c.js'],
['C:', 'C:'],
['C:..\\..\\abc\\..\\def', 'def'],
['C:..\\abc', 'C:../abc'],
['C:\\.', 'C:'],
['\\\\server\\share\\dir\\file.ext', 'server/share/dir/file.ext'],
['\\a\\b\\c\\..\\..\\..\\x\\y\\z', 'x/y/z'],
['\\foo\\..\\..\\..\\bar', 'bar'],
['a\\\\b\\\\.', 'a/b'],
['a\\\\b\\\\..\\b', 'a/b'],
['a\\\\b\\\\.\\c', 'a/b/c'],
['bar\\foo..', 'bar/foo..'],
['bar\\foo..\\', 'bar/foo..'],
['bar\\foo..\\..', 'bar'],
['bar\\foo..\\..\\', 'bar'],
['bar\\foo..\\..\\baz', 'bar/baz'],
['file:stream', 'file:stream'],
['foo\\bar\\baz', 'foo/bar/baz']
]

// Act + Expect
cases.forEach(([path, expected]) => {
expect(testSubject(path)).to.equal(expected)
})
})
})
})
13 changes: 4 additions & 9 deletions src/internal/ensure-posix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,18 @@ import validateString from './validate-string'
*
* - Converting Windows-style path delimiters (`;`) to POSIX (`:`)
* - Converting Windows-style path segment separators (`\`) to POSIX (`/`)
* - Consolidating duplicate path segment separators (occurs when an escaped
* Windows-style separator (`\\`) is converted to POSIX)
*
* @see https://nodejs.org/api/path.html#windows-vs-posix
* @see https://nodejs.org/api/path.html#pathdelimiter
* @see https://nodejs.org/api/path.html#pathsep
*
* @param {string} [path=''] - Path to normalize
* @return {string} `path` normalized
* @param {string} [path=''] - Path to ensure
* @return {string} POSIX-compliant `path`
* @throws {TypeError} If `path` is not a string
*/
const ensurePosix = (path: string = ''): string => {
validateString(path, 'path')

return path
.replace(/;/g, delimiter) // convert path delimiters
.replace(/\\/g, sep) // convert path segment separators
.replace(new RegExp(`(?<!^)\\${sep}+`), sep) // consolidate separators
return path.replace(/;/g, delimiter).replace(/\\/g, sep)
}

export default ensurePosix
170 changes: 170 additions & 0 deletions src/internal/normalize-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* @file Internal - normalizeString
* @module pathe/internal/normalizeString
*/

import isAbsolute from '#src/lib/is-absolute'
import sep from '#src/lib/sep'
import { DOT } from './constants'
import ensurePosix from './ensure-posix'
import isSep from './is-sep'
import validateString from './validate-string'

/**
* Normalizes a path.
*
* This includes:
*
* - Enforcing POSIX standards
* - Resolving `'.'` (current directory) and `'..'` (parent directory) segments
* - Deduplicating [separators][1]. Leading and trailing separators are **not**
* preserved
*
* [1]: {@link ../lib/sep.ts}
*
* @param {string} path - Path to normalize
* @return {string} Normalized `path`
* @throws {TypeError} If `path` is not a string
*/
const normalizeString = (path: string): string => {
validateString(path, 'path')

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

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

// exit early if path does not contain dot characters or separators
if (!path.includes(DOT) && !path.includes(sep)) return path

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

/**
* Current character in {@link path} being processed.
*
* @var {string | undefined} char
*/
let char: string | undefined

/**
* Total number of `.` (dot) characters in current path segment.
*
* @var {number} dots
*/
let dots: number = 0

/**
* Normalized {@linkcode path}.
*
* @var {string} res
*/
let res: string = ''

/**
* Last path segment length.
*
* @var {number} seglen
*/
let seglen: number = 0

/**
* Last path separator index.
*
* @var {number} sepidx
*/
let sepidx: number = -1

// normalize
for (let i = 0; i <= path.length; ++i) {
// set current character if current index is in bounds
if (i < path.length) char = path[i]
// exit if trailing separator has been reached
else if (isSep(char)) break
// add trailing separator
else char = sep

// start or end segment
if (isSep(char)) {
if (sepidx === i - 1 || dots === 1) {
// noop: leading or duplicate separator, or segment with 1 dot character
} else if (dots === 2) {
// resolve ".." segment
if (res.length < 2 || seglen !== 2 || !/(?:\..$)|(?:\.$)/.test(res)) {
if (res.length > 2) {
/**
* Index of last path separator in {@linkcode res}.
*
* @const {number} sepidx_res
*/
const sepidx_res: number = res.lastIndexOf(sep)

// reset result and last segment length if sep was not found in res
if (sepidx_res === -1) {
res = ''
seglen = 0
} else {
// end result at index of last separator
res = res.slice(0, sepidx_res)

// reset last segment length
seglen = res.length - 1 - res.lastIndexOf(sep)
}

// set last seperator index and reset dot character count
sepidx = i
dots = 0

continue
} else if (res.length > 0) {
// set last seperator index
sepidx = i

// reset dot character count, result, and last segment length
dots = 0
res = ''
seglen = 0

continue
}
}

// resolve past root if path is not absolute
if (!absolute) {
res += `${!res ? '' : sep}${DOT.repeat(2)}`
seglen = 2
}
} else {
if (res.length > 0) {
// add segment
res += `${sep}${path.slice(sepidx + 1, i)}`
} else {
// reset result to segment
res = path.slice(sepidx + 1, i)
}

// reset last segment length
seglen = i - sepidx - 1
}

// set last seperator index and reset dot character count
sepidx = i
dots = 0
} else if (char === DOT && dots !== -1) {
// encountered segment that is reference to parent directory ("..")
++dots
} else {
// iterated over segment
dots = -1
}
}

return res
}

export default normalizeString

0 comments on commit 1cf683e

Please sign in to comment.