-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
extname.ts
136 lines (118 loc) · 3.24 KB
/
extname.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
* @file extname
* @module pathe/lib/extname
*/
import ensurePosix from '#src/internal/ensure-posix'
import isDrivePath from '#src/internal/is-drive-path'
import isSep from '#src/internal/is-sep'
import validateString from '#src/internal/validate-string'
import type { Ext } from '#src/types'
import {
DOT,
at,
cast,
includes,
type EmptyString
} from '@flex-development/tutils'
/**
* Returns the extension of a `path`, from the last occurrence of the `.` (dot)
* character to end of the string in the last portion of the path.
*
* If there is no `.` in the last portion of `path`, or if there are no `.`
* characters other than the first character of the path's [`basename`][1], an
* empty string will be returned.
*
* [1]: {@link ./basename.ts}
*
* @param {string} path - Path to evaluate
* @return {EmptyString | Ext} Extension of `path` or empty string
* @throws {TypeError} If `path` is not a string
*/
const extname = (path: string): EmptyString | Ext => {
validateString(path, 'path')
// exit early if path does not contain any dot characters
if (!includes(path, DOT)) return ''
// ensure path meets posix standards
path = ensurePosix(path)
/**
* Index to begin searching for extension.
*
* @var {number} offset
*/
let offset: number = 0
/**
* Start index of {@linkcode path}'s basename.
*
* @var {number} part
*/
let part: number = 0
/**
* State of characters, if any, before first dot character and after any path
* separators in {@linkcode path}.
*
* @var {number} predot
*/
let predot: number = 0
/**
* Directory separator match check.
*
* @var {boolean} sep_match
*/
let sep_match: boolean = true
/**
* Start index of extension.
*
* @var {number} start
*/
let start: number = -1
/**
* End index of extension.
*
* @var {number} end
*/
let end: number = -1
// check for drive path so as not to mistake the following path separator as
// an extra separator at the end of the path that can be disregarded
if (path.length >= 2 && isDrivePath(path)) {
offset = part = 2
}
// get start and end indices of extension
for (let i = path.length - 1; i >= offset; --i) {
/**
* Character at {@linkcode i} in {@linkcode path}.
*
* @const {string} char
*/
const char: string = at(path, i)
// adjust start index of basename
if (isSep(char)) {
if (!sep_match) {
// encountered separator that is not trailing separator
part = i + 1
break
}
continue
}
// set end index of extension
if (end === -1) {
end = i + 1
sep_match = false
}
// set start index of extension and/or update predot state
if (char === DOT) {
// set start index of extension
if (start === -1) start = i
else if (predot !== 1) predot = 1
} else if (start !== -1) {
// a non-dot and non-separator character was encountered before the dot
// character, so there is a good chance at having a non-empty extension
predot = -1
}
}
return start === -1 || end === -1 || predot === 0
? ''
: start === part + 1 && start === end - 1 && predot === 1
? ''
: cast<Ext>(path.slice(start, end))
}
export default extname