@oazmi/superbuild 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/_dnt.shims.d.ts +2 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/_dnt.shims.js +57 -0
- package/esm/deps/jsr.io/@oazmi/esbuild-types/0.28.0/src/mod.d.ts +598 -0
- package/esm/deps/jsr.io/@oazmi/esbuild-types/0.28.0/src/mod.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/esbuild-types/0.28.0/src/mod.js +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/alias.d.ts +617 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/alias.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/alias.js +671 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array1d.d.ts +487 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array1d.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array1d.js +536 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array2d.d.ts +266 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array2d.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/array2d.js +318 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/binder.d.ts +368 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/binder.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/binder.js +380 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/crossenv.d.ts +711 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/crossenv.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/crossenv.js +1173 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/cryptoman.d.ts +171 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/cryptoman.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/cryptoman.js +308 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/deps.d.ts +10 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/deps.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/deps.js +10 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack.d.ts +168 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack.js +236 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack_varint.d.ts +90 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack_varint.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/eightpack_varint.js +237 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericarray.d.ts +213 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericarray.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericarray.js +363 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericmethods.d.ts +233 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericmethods.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/numericmethods.js +256 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/pathman.d.ts +1436 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/pathman.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/pathman.js +1730 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/promiseman.d.ts +647 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/promiseman.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/promiseman.js +762 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/stringman.d.ts +538 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/stringman.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/stringman.js +626 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/struct.d.ts +734 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/struct.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/struct.js +764 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedbuffer.d.ts +137 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedbuffer.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedbuffer.js +234 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedefs.d.ts +391 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedefs.d.ts.map +1 -0
- package/esm/deps/jsr.io/@oazmi/kitchensink/0.10.0/src/typedefs.js +8 -0
- package/esm/deps.d.ts +56 -0
- package/esm/deps.d.ts.map +1 -0
- package/esm/deps.js +37 -0
- package/esm/esbuild/metafile.d.ts +90 -0
- package/esm/esbuild/metafile.d.ts.map +1 -0
- package/esm/esbuild/metafile.js +275 -0
- package/esm/esbuild/native.d.ts +95 -0
- package/esm/esbuild/native.d.ts.map +1 -0
- package/esm/esbuild/native.js +127 -0
- package/esm/esbuild/outputfile.d.ts +86 -0
- package/esm/esbuild/outputfile.d.ts.map +1 -0
- package/esm/esbuild/outputfile.js +221 -0
- package/esm/esbuild/strongtypes.d.ts +90 -0
- package/esm/esbuild/strongtypes.d.ts.map +1 -0
- package/esm/esbuild/strongtypes.js +8 -0
- package/esm/esbuild/typedefs.d.ts +121 -0
- package/esm/esbuild/typedefs.d.ts.map +1 -0
- package/esm/esbuild/typedefs.js +55 -0
- package/esm/esbuild/weaktypes.d.ts +46 -0
- package/esm/esbuild/weaktypes.d.ts.map +1 -0
- package/esm/esbuild/weaktypes.js +8 -0
- package/esm/funcdefs.d.ts +89 -0
- package/esm/funcdefs.d.ts.map +1 -0
- package/esm/funcdefs.js +145 -0
- package/esm/mod.d.ts +4 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +2 -0
- package/esm/package.json +3 -0
- package/esm/plugins/emissions_driver.d.ts +33 -0
- package/esm/plugins/emissions_driver.d.ts.map +1 -0
- package/esm/plugins/emissions_driver.js +260 -0
- package/esm/plugins/long_build.d.ts +138 -0
- package/esm/plugins/long_build.d.ts.map +1 -0
- package/esm/plugins/long_build.js +288 -0
- package/esm/plugins/native_replica.d.ts +48 -0
- package/esm/plugins/native_replica.d.ts.map +1 -0
- package/esm/plugins/native_replica.js +122 -0
- package/esm/super/build.d.ts +53 -0
- package/esm/super/build.d.ts.map +1 -0
- package/esm/super/build.js +39 -0
- package/esm/super/build_context.d.ts +83 -0
- package/esm/super/build_context.d.ts.map +1 -0
- package/esm/super/build_context.js +124 -0
- package/esm/super/mod.d.ts +12 -0
- package/esm/super/mod.d.ts.map +1 -0
- package/esm/super/mod.js +9 -0
- package/esm/super/plugin.d.ts +23 -0
- package/esm/super/plugin.d.ts.map +1 -0
- package/esm/super/plugin.js +31 -0
- package/esm/super/plugin_build.d.ts +38 -0
- package/esm/super/plugin_build.d.ts.map +1 -0
- package/esm/super/plugin_build.js +198 -0
- package/esm/super/typedefs.d.ts +323 -0
- package/esm/super/typedefs.d.ts.map +1 -0
- package/esm/super/typedefs.js +10 -0
- package/esm/typedefs.d.ts +21 -0
- package/esm/typedefs.d.ts.map +1 -0
- package/esm/typedefs.js +5 -0
- package/license.md +37 -0
- package/package.json +88 -0
- package/readme.md +181 -0
|
@@ -0,0 +1,1730 @@
|
|
|
1
|
+
/** utility tools for manipulating paths and obtaining `URL`s.
|
|
2
|
+
*
|
|
3
|
+
* TODO: write document level examples.
|
|
4
|
+
*
|
|
5
|
+
* url terminology:
|
|
6
|
+
* - urls are a subset of uris
|
|
7
|
+
* - a url protocol is defined as: `[scheme]://`
|
|
8
|
+
* - a url is defined as: `[scheme]://[host]/[path]?[queryString]#[fragmentHash]`
|
|
9
|
+
* - or equivalently, a url is: `[protocol][host]/[path]?[queryString]#[fragmentHash]`
|
|
10
|
+
* - a uri is defined as: `[scheme]:[someIdentifier]`
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import { array_from, dom_decodeURI, dom_encodeURI, object_entries } from "./alias.js";
|
|
15
|
+
import { DEBUG } from "./deps.js";
|
|
16
|
+
import { commonPrefix, quote } from "./stringman.js";
|
|
17
|
+
import { isObject, isString } from "./struct.js";
|
|
18
|
+
// DONE: consider if it would be a good idea to export `uri_protocol_and_scheme_mapping` so that the end user can manually modify it to their needs,
|
|
19
|
+
// and then have this whole submodule behave according to their custom uri scheme definitions.
|
|
20
|
+
// of course we're going to encounter issues with the `UriScheme` type, and might even have to turn it into a `string` (there by killing off all typing benefits),
|
|
21
|
+
// but it's still less painful than having the end user having to redefine all path resolution functions via wrappers.
|
|
22
|
+
/** this is global mapping of uri-protocol schemes that are identifiable by {@link getUriScheme} and {@link resolveAsUrl}.
|
|
23
|
+
* you may mutate this 2-tuple array to add or remove custom identifiable uri-schemes.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* adding a new uri protocol scheme named `"inline-scheme"` to our registry:
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { assertEquals, assertThrows } from "jsr:@std/assert"
|
|
29
|
+
*
|
|
30
|
+
* // aliasing our functions for brevity
|
|
31
|
+
* const eq = assertEquals, err = assertThrows
|
|
32
|
+
*
|
|
33
|
+
* // initially, our custom "inline" scheme is unidentifiable, and cannot be used in `resolveAsUrl` as a base url
|
|
34
|
+
* eq(getUriScheme("inline://a/b/c.txt"), "relative")
|
|
35
|
+
* err(() => resolveAsUrl("./w.xyz", "inline://a/b/c.txt")) // "inline://a/b/c.txt" is identified as a relative path, and cannot be used as a base path
|
|
36
|
+
*
|
|
37
|
+
* // registering the custom protocol-scheme mapping.
|
|
38
|
+
* // note that you will have to declare `as any`, since the schemes are tightly defined by the type `UriScheme`.
|
|
39
|
+
* uriProtocolSchemeMap.push(["inline://", "inline-scheme" as any])
|
|
40
|
+
*
|
|
41
|
+
* // and now, our custom "inline" scheme becomes identifiable
|
|
42
|
+
* eq(getUriScheme("inline://a/b/c.txt"), "inline-scheme")
|
|
43
|
+
*
|
|
44
|
+
* // it is also now accepted by `resolveAsUrl` as a base uri
|
|
45
|
+
* eq(resolveAsUrl("./w.xyz", "inline://a/b/c.txt"), new URL("inline://a/b/w.xyz"))
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export const uriProtocolSchemeMap = /*@__PURE__*/ object_entries({
|
|
49
|
+
"node:": "node",
|
|
50
|
+
"npm:": "npm",
|
|
51
|
+
"jsr:": "jsr",
|
|
52
|
+
"blob:": "blob",
|
|
53
|
+
"data:": "data",
|
|
54
|
+
"http://": "http",
|
|
55
|
+
"https://": "https",
|
|
56
|
+
"file://": "file",
|
|
57
|
+
"./": "relative",
|
|
58
|
+
"../": "relative",
|
|
59
|
+
});
|
|
60
|
+
/** here, you can specify which uri schemes cannot be used as a base url for resolving a url via the {@link resolveAsUrl} function.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* adding a new uri protocol scheme named `"base64-scheme"` to our registry, and then forbidding it from being used as a base url:
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { assertEquals, assertThrows } from "jsr:@std/assert"
|
|
66
|
+
*
|
|
67
|
+
* // aliasing our functions for brevity
|
|
68
|
+
* const eq = assertEquals, err = assertThrows
|
|
69
|
+
*
|
|
70
|
+
* // initially, our custom "base64" scheme is unidentifiable, and cannot be used in `resolveAsUrl` as a base url
|
|
71
|
+
* eq(getUriScheme("base64://a/b/c.txt"), "relative")
|
|
72
|
+
* err(() => resolveAsUrl("./w.xyz", "base64://a/b/c.txt")) // "base64://a/b/c.txt" is identified as a relative path, and cannot be used as a base path
|
|
73
|
+
*
|
|
74
|
+
* // registering the custom protocol-scheme mapping.
|
|
75
|
+
* // note that you will have to declare `as any`, since the schemes are tightly defined by the type `UriScheme`.
|
|
76
|
+
* uriProtocolSchemeMap.push(["base64://", "base64-scheme" as any])
|
|
77
|
+
*
|
|
78
|
+
* // and now, our custom "base64" scheme becomes identifiable.
|
|
79
|
+
* eq(getUriScheme("base64://a/b/c.txt"), "base64-scheme")
|
|
80
|
+
*
|
|
81
|
+
* // it is also now accepted by `resolveAsUrl` as a base uri
|
|
82
|
+
* eq(resolveAsUrl("./w.xyz", "base64://a/b/c.txt"), new URL("base64://a/b/w.xyz"))
|
|
83
|
+
*
|
|
84
|
+
* // since we don't want to make it possible to have "base64-scheme" as a base uri, so we'll put it in the forbidden list.
|
|
85
|
+
* // once again `as any` is needed, since the `UriScheme` is tightly defined, and its definition cannot be changed.
|
|
86
|
+
* forbiddenBaseUriSchemes.push("base64-scheme" as any)
|
|
87
|
+
* err(() => resolveAsUrl("./w.xyz", "base64://a/b/c.txt")) // "base64://a/b/c.txt" is now amongst the forbidden schemes that cannot be combined with relative paths.
|
|
88
|
+
* eq(resolveAsUrl("base64://a/b/c.txt"), new URL("base64://a/b/c.txt")) // this is of course not stopping us from building urls with the "base64" scheme, so long as no relative path is attached.
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export const forbiddenBaseUriSchemes = ["blob", "data", "relative"];
|
|
92
|
+
const packageUriSchemes = ["jsr", "npm", "node"], packageUriProtocols = ["jsr:", "npm:", "node:"];
|
|
93
|
+
const
|
|
94
|
+
// posix directory path separator
|
|
95
|
+
sep = "/",
|
|
96
|
+
// posix relative directory path navigator
|
|
97
|
+
dotslash = "./",
|
|
98
|
+
// posix relative parent directory path navigator
|
|
99
|
+
dotdotslash = "../",
|
|
100
|
+
// regex for attaining windows directory path separator ("\\")
|
|
101
|
+
windows_directory_slash_regex = /\\/g,
|
|
102
|
+
// regex for detecting if a path is an absolute windows path
|
|
103
|
+
windows_absolute_path_regex = /^[a-z]\:[\/\\]/i,
|
|
104
|
+
// regex for correcting an invalid single leading slash in a windows path
|
|
105
|
+
windows_leading_slash_correction_regex = /^[\/\\]([a-z])\:[\/\\]/i,
|
|
106
|
+
// regex for attaining leading consecutive slashes
|
|
107
|
+
leading_slashes_regex = /^\/+/,
|
|
108
|
+
// regex for attaining trailing consecutive slashes, except for those that are preceded by a dotslash ("/./") or a dotdotslash ("/../")
|
|
109
|
+
trailing_slashes_regex = /(?<!\/\.\.?)\/+$/,
|
|
110
|
+
// regex for attaining leading consecutive slashes and dot-slashes
|
|
111
|
+
leading_slashes_and_dot_slashes_regex = /^(\.?\/)+/,
|
|
112
|
+
// regex for attaining a reversed path string's trailing consecutive slashes and dot-slashes, but not the slashes preceded by a dotdotslash ("/..").
|
|
113
|
+
// this regex is a little complex, so you might want to check out the test cases (of reversed path strings) here: "https://regex101.com/r/IV0twv/1"
|
|
114
|
+
reversed_trailing_slashes_and_dot_slashes_regex = /^(\/\.?(?![^\/]))*(\/(?!\.\.\/))?/,
|
|
115
|
+
// regex for attaining the file name of a path, including its leading slash (if there is one)
|
|
116
|
+
filename_regex = /\/?[^\/]+$/,
|
|
117
|
+
// regex for attaining the base name and extension name of a file, from its filename (no directories)
|
|
118
|
+
basename_and_extname_regex = /^(?<basename>.+?)(?<ext>\.[^\.]+)?$/,
|
|
119
|
+
// an npm or jsr package string parsing regex. see the test cases on regex101 link: "https://regex101.com/r/mX3v1z/2"
|
|
120
|
+
package_regex = /^(?<protocol>jsr:|npm:|node:)(\/*(@(?<scope>[^\/\s]+)\/)?(?<pkg>[^@\/\s]+)(@(?<version>[^\/\r\n\t\f\v]+))?)?(?<pathname>\/.*)?$/, string_starts_with = (str, starts_with) => str.startsWith(starts_with), string_ends_with = (str, ends_with) => str.endsWith(ends_with);
|
|
121
|
+
/** test whether a given path is an absolute path (either windows or posix).
|
|
122
|
+
*
|
|
123
|
+
* > [!note]
|
|
124
|
+
* > currently, we do consider the tilde expansion ("~") as an absolute path, even though it is not an os/fs-level path, but rather a shell feature.
|
|
125
|
+
* > this may result in misclassification on windows, since "~" is a valid starting character for a file or folder name
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
130
|
+
*
|
|
131
|
+
* // aliasing our functions for brevity
|
|
132
|
+
* const eq = assertEquals, fn = isAbsolutePath
|
|
133
|
+
*
|
|
134
|
+
* eq(fn("/a/b/c.txt"), true)
|
|
135
|
+
* eq(fn("~/a/b/c.txt"), true)
|
|
136
|
+
* eq(fn("C:/a/b/c.txt"), true)
|
|
137
|
+
* eq(fn("/c:/a/b/c.txt"), true)
|
|
138
|
+
*
|
|
139
|
+
* eq(fn("a/b/c.txt"), false)
|
|
140
|
+
* eq(fn("./a/b/c.txt"), false)
|
|
141
|
+
* eq(fn("../a/b/c.txt"), false)
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export const isAbsolutePath = (path) => {
|
|
145
|
+
return (string_starts_with(path, sep)
|
|
146
|
+
|| string_starts_with(path, "~")
|
|
147
|
+
|| windows_absolute_path_regex.test(path));
|
|
148
|
+
};
|
|
149
|
+
/** guesses the scheme of a url string. see {@link UriScheme} for more details.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
154
|
+
*
|
|
155
|
+
* // aliasing our functions for brevity
|
|
156
|
+
* const eq = assertEquals, fn = getUriScheme
|
|
157
|
+
*
|
|
158
|
+
* eq(fn("C:/Users/me/path/to/file.txt"), "local")
|
|
159
|
+
* eq(fn("~/path/to/file.txt"), "local")
|
|
160
|
+
* eq(fn("/usr/me/path/to/file.txt"), "local")
|
|
161
|
+
* eq(fn("path/to/file.txt"), "relative")
|
|
162
|
+
* eq(fn("./path/to/file.txt"), "relative")
|
|
163
|
+
* eq(fn("../path/to/file.txt"), "relative")
|
|
164
|
+
* eq(fn("file:///c://users/me/path/to/file.txt"), "file")
|
|
165
|
+
* eq(fn("file:///usr/me/path/to/file.txt"), "file")
|
|
166
|
+
* eq(fn("jsr:@user/path/to/file"), "jsr")
|
|
167
|
+
* eq(fn("jsr:/@user/path/to/file"), "jsr")
|
|
168
|
+
* eq(fn("npm:lib/path/to/file"), "npm")
|
|
169
|
+
* eq(fn("npm:/lib/path/to/file"), "npm")
|
|
170
|
+
* eq(fn("npm:/@scope/lib/path/to/file"), "npm")
|
|
171
|
+
* eq(fn("node:http"), "node")
|
|
172
|
+
* eq(fn("node:fs/promises"), "node")
|
|
173
|
+
* eq(fn("data:text/plain;charset=utf-8;base64,aGVsbG8="), "data")
|
|
174
|
+
* eq(fn("blob:https://example.com/4800d2d8-a78c-4895"), "blob")
|
|
175
|
+
* eq(fn("http://google.com/style.css"), "http")
|
|
176
|
+
* eq(fn("https://google.com/style.css"), "https")
|
|
177
|
+
* eq(fn(""), undefined)
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
export const getUriScheme = (path) => {
|
|
181
|
+
if (!path || path === "") {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
for (const [protocol, scheme] of uriProtocolSchemeMap) {
|
|
185
|
+
if (string_starts_with(path, protocol)) {
|
|
186
|
+
return scheme;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return isAbsolutePath(path) ? "local" : "relative";
|
|
190
|
+
};
|
|
191
|
+
/** this function parses npm and jsr package strings, and returns a pseudo URL-like object.
|
|
192
|
+
*
|
|
193
|
+
* the regex we use for parsing the input `href` string is quoted below:
|
|
194
|
+
* > /^(?<protocol>jsr:|npm:|node:)(\/*(@(?<scope>[^\/\s]+)\/)?(?<pkg>[^@\/\s]+)(@(?<version>[^\/\r\n\t\f\v]+))?)?(?<pathname>\/.*)?$/
|
|
195
|
+
*
|
|
196
|
+
* see the regex in action with the test cases on regex101 link: [regex101.com/r/mX3v1z/2](https://regex101.com/r/mX3v1z/2)
|
|
197
|
+
*
|
|
198
|
+
* @throws `Error` an error will be thrown if either the package name (`pkg`), or the `protocol` cannot be deduced by the regex.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```ts
|
|
202
|
+
* import { assertEquals, assertThrows } from "jsr:@std/assert"
|
|
203
|
+
*
|
|
204
|
+
* // aliasing our functions for brevity
|
|
205
|
+
* const eq = assertEquals, err = assertThrows, fn = parsePackageUrl
|
|
206
|
+
*
|
|
207
|
+
* // basic breakdown of a package's resource uri
|
|
208
|
+
* eq(fn("jsr:@scope/package@version/pathname/file.ts"), {
|
|
209
|
+
* href: "jsr:/@scope/package@version/pathname/file.ts",
|
|
210
|
+
* protocol: "jsr:",
|
|
211
|
+
* scope: "scope",
|
|
212
|
+
* pkg: "package",
|
|
213
|
+
* version: "version",
|
|
214
|
+
* pathname: "/pathname/file.ts",
|
|
215
|
+
* host: "@scope/package@version",
|
|
216
|
+
* })
|
|
217
|
+
*
|
|
218
|
+
* // showing that jsr package uri's without a scope are perfectly permitted.
|
|
219
|
+
* // even though it isn't actually possible to do so on "jsr.io".
|
|
220
|
+
* // thus it is left up to the end-user to make of it what they will.
|
|
221
|
+
* eq(fn("jsr:package@version/pathname/"), {
|
|
222
|
+
* href: "jsr:/package@version/pathname/",
|
|
223
|
+
* protocol: "jsr:",
|
|
224
|
+
* scope: undefined,
|
|
225
|
+
* pkg: "package",
|
|
226
|
+
* version: "version",
|
|
227
|
+
* pathname: "/pathname/",
|
|
228
|
+
* host: "package@version",
|
|
229
|
+
* })
|
|
230
|
+
*
|
|
231
|
+
* // testing a case with multiple slashes ("/") after the protocol colon (":"), and no trailing slash after the version
|
|
232
|
+
* eq(fn("npm:///@scope/package@version"), {
|
|
233
|
+
* href: "npm:/@scope/package@version/",
|
|
234
|
+
* protocol: "npm:",
|
|
235
|
+
* scope: "scope",
|
|
236
|
+
* pkg: "package",
|
|
237
|
+
* version: "version",
|
|
238
|
+
* pathname: "/",
|
|
239
|
+
* host: "@scope/package@version",
|
|
240
|
+
* })
|
|
241
|
+
*
|
|
242
|
+
* // testing a no-scope and no-version case
|
|
243
|
+
* eq(fn("npm:package"), {
|
|
244
|
+
* href: "npm:/package/",
|
|
245
|
+
* protocol: "npm:",
|
|
246
|
+
* scope: undefined,
|
|
247
|
+
* pkg: "package",
|
|
248
|
+
* version: undefined,
|
|
249
|
+
* pathname: "/",
|
|
250
|
+
* host: "package",
|
|
251
|
+
* })
|
|
252
|
+
*
|
|
253
|
+
* // testing the "node:" protocol
|
|
254
|
+
* eq(fn("node:fs"), {
|
|
255
|
+
* href: "node:/fs/",
|
|
256
|
+
* protocol: "node:",
|
|
257
|
+
* scope: undefined,
|
|
258
|
+
* pkg: "fs",
|
|
259
|
+
* version: undefined,
|
|
260
|
+
* pathname: "/",
|
|
261
|
+
* host: "fs",
|
|
262
|
+
* })
|
|
263
|
+
*
|
|
264
|
+
* // testing the "node:" protocol with a certain pathname
|
|
265
|
+
* eq(fn("node:fs/promises"), {
|
|
266
|
+
* href: "node:/fs/promises",
|
|
267
|
+
* protocol: "node:",
|
|
268
|
+
* scope: undefined,
|
|
269
|
+
* pkg: "fs",
|
|
270
|
+
* version: undefined,
|
|
271
|
+
* pathname: "/promises",
|
|
272
|
+
* host: "fs",
|
|
273
|
+
* })
|
|
274
|
+
*
|
|
275
|
+
* // testing a `version` query string that contains whitespaces and url-encoded characters.
|
|
276
|
+
* // NOTE: the url-encoded characters in vs-code's doc popup appear decoded, so don't be fooled!
|
|
277
|
+
* // but the `host` is always a url-decoded string.
|
|
278
|
+
* eq(fn("jsr:@scope/package@1.0.0 - 1.2.0/pathname/file.ts"), {
|
|
279
|
+
* href: "jsr:/@scope/package@1.0.0%20-%201.2.0/pathname/file.ts",
|
|
280
|
+
* protocol: "jsr:",
|
|
281
|
+
* scope: "scope",
|
|
282
|
+
* pkg: "package",
|
|
283
|
+
* version: "1.0.0 - 1.2.0",
|
|
284
|
+
* pathname: "/pathname/file.ts",
|
|
285
|
+
* host: "@scope/package@1.0.0 - 1.2.0",
|
|
286
|
+
* })
|
|
287
|
+
*
|
|
288
|
+
* // testing a `version` query string that has its some of its characters (such as whitespaces) url-encoded.
|
|
289
|
+
* // NOTE: the url-encoded characters in vs-code's doc popup appear decoded, so don't be fooled!
|
|
290
|
+
* // but the `host` is always a url-decoded string.
|
|
291
|
+
* eq(fn("jsr:@scope/package@^2%20<2.2%20||%20>%202.3/pathname/file.ts"), {
|
|
292
|
+
* href: "jsr:/@scope/package@%5E2%20%3C2.2%20%7C%7C%20%3E%202.3/pathname/file.ts",
|
|
293
|
+
* protocol: "jsr:",
|
|
294
|
+
* scope: "scope",
|
|
295
|
+
* pkg: "package",
|
|
296
|
+
* version: "^2 <2.2 || > 2.3",
|
|
297
|
+
* pathname: "/pathname/file.ts",
|
|
298
|
+
* host: "@scope/package@^2 <2.2 || > 2.3",
|
|
299
|
+
* })
|
|
300
|
+
*
|
|
301
|
+
* // testing cases where an error should be invoked
|
|
302
|
+
* err(() => fn("npm:@scope/")) // missing a package name
|
|
303
|
+
* err(() => fn("npm:@scope//package")) // more than one slash after scope
|
|
304
|
+
* err(() => fn("pnpm:@scope/package@version")) // only "node:", "npm:", and "jsr:" protocols are recognized
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
export const parsePackageUrl = (url_href) => {
|
|
308
|
+
url_href = dom_decodeURI(isString(url_href) ? url_href : url_href.href);
|
|
309
|
+
const { protocol, scope: scope_str, pkg, version: version_str, pathname: pathname_str } = package_regex.exec(url_href)?.groups ?? {};
|
|
310
|
+
if ((protocol === undefined) || (pkg === undefined)) {
|
|
311
|
+
throw new Error(DEBUG.ERROR ? ("invalid package url format was provided: " + url_href) : "");
|
|
312
|
+
}
|
|
313
|
+
const scope = scope_str ? scope_str : undefined, // turn empty strings into `undefined`
|
|
314
|
+
version = version_str ? version_str : undefined, // turn empty strings into `undefined`
|
|
315
|
+
pathname = pathname_str ? pathname_str : sep, // pathname must always begin with a leading slash, even if it was originally empty
|
|
316
|
+
host = `${scope ? "@" + scope + sep : ""}${pkg}${version ? "@" + version : ""}`, href = dom_encodeURI(`${protocol}/${host}${pathname}`);
|
|
317
|
+
return {
|
|
318
|
+
protocol: protocol,
|
|
319
|
+
scope, pkg, version, pathname, host, href,
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
/** convert a url string to an actual `URL` object.
|
|
323
|
+
* your input `path` url can use any scheme supported by the {@link getUriScheme} function.
|
|
324
|
+
* and you may also use paths with windows dir-separators ("\\"), as this function implicitly converts them a posix separator ("/").
|
|
325
|
+
*
|
|
326
|
+
* if you pass a `URL` object, then it will be returned as is.
|
|
327
|
+
*
|
|
328
|
+
* @throws `Error` an error will be thrown if `base` uri is either a relative path, or uses a data uri scheme,
|
|
329
|
+
* or if the provided `path` is relative, but no absolute `base` path is provided.
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```ts
|
|
333
|
+
* import { assertEquals, assertThrows } from "jsr:@std/assert"
|
|
334
|
+
*
|
|
335
|
+
* // aliasing our functions for brevity
|
|
336
|
+
* const eq = assertEquals, err = assertThrows, fn = resolveAsUrl
|
|
337
|
+
*
|
|
338
|
+
* eq(fn(new URL("some://url:8000/a/b c.txt")), new URL("some://url:8000/a/b%20c.txt"))
|
|
339
|
+
*
|
|
340
|
+
* eq(fn("/a/b/c d e.txt"), new URL("file:///a/b/c%20d%20e.txt"))
|
|
341
|
+
* eq(fn("~/a/b/c.txt"), new URL("file://~/a/b/c.txt"))
|
|
342
|
+
* eq(fn("C:/a/b/c/d/e.txt"), new URL("file:///C:/a/b/c/d/e.txt"))
|
|
343
|
+
* eq(fn("C:\\a\\b\\c\\d\\e.txt"), new URL("file:///C:/a/b/c/d/e.txt"))
|
|
344
|
+
* eq(fn("./e/f g.txt", "C:/a\\b\\c d/"), new URL("file:///C:/a/b/c%20d/e/f%20g.txt"))
|
|
345
|
+
* eq(fn("../c d/e/f g.txt", "C:/a/b/c d/"), new URL("file:///C:/a/b/c%20d/e/f%20g.txt"))
|
|
346
|
+
* eq(fn("d/../.././e.txt", "C:/a/b/c/"), new URL("file:///C:/a/b/e.txt"))
|
|
347
|
+
* eq(fn("d/../.././e.txt", "C:/a/b/c"), new URL("file:///C:/a/e.txt"))
|
|
348
|
+
* eq(fn("D:/a/b.txt", "C:/c/d.txt"), new URL("file:///D:/a/b.txt"))
|
|
349
|
+
* eq(fn("/a/b.txt", "C:/c/d.txt"), new URL("file:///C:/a/b.txt"))
|
|
350
|
+
* eq(fn("/a/b.txt", "/sys/admin/"), new URL("file:///a/b.txt"))
|
|
351
|
+
* eq(fn("/a/b.txt", ""), new URL("file:///a/b.txt"))
|
|
352
|
+
*
|
|
353
|
+
* eq(fn("http://cdn.esm.sh/a/b/c.txt"), new URL("http://cdn.esm.sh/a/b/c.txt"))
|
|
354
|
+
* eq(fn("http://cdn.esm.sh/a.txt", "file:///b/"), new URL("http://cdn.esm.sh/a.txt"))
|
|
355
|
+
* eq(fn("http://cdn.esm.sh/a.txt", "/b/"), new URL("http://cdn.esm.sh/a.txt"))
|
|
356
|
+
* eq(fn("/a/b/c.txt", "http://cdn.esm.sh/"), new URL("http://cdn.esm.sh/a/b/c.txt"))
|
|
357
|
+
*
|
|
358
|
+
* eq(fn("b/c.txt", "http://cdn.esm.sh/a/"), new URL("http://cdn.esm.sh/a/b/c.txt"))
|
|
359
|
+
* eq(fn("b/c.txt", "http://cdn.esm.sh/a"), new URL("http://cdn.esm.sh/b/c.txt"))
|
|
360
|
+
* eq(fn("./b/c.txt", "http://cdn.esm.sh/a/"), new URL("http://cdn.esm.sh/a/b/c.txt"))
|
|
361
|
+
* eq(fn("./b/c.txt", "http://cdn.esm.sh/a"), new URL("http://cdn.esm.sh/b/c.txt"))
|
|
362
|
+
* eq(fn("../b/c.txt", "https://cdn.esm.sh/a/"), new URL("https://cdn.esm.sh/b/c.txt"))
|
|
363
|
+
* eq(fn("../c/d.txt", "https://cdn.esm.sh/a/b"), new URL("https://cdn.esm.sh/c/d.txt"))
|
|
364
|
+
* eq(fn("/c/d.txt", "https://cdn.esm.sh/a/b"), new URL("https://cdn.esm.sh/c/d.txt"))
|
|
365
|
+
*
|
|
366
|
+
* eq(fn("node:fs"), new URL("node:/fs/"))
|
|
367
|
+
* eq(fn("node:fs/promises"), new URL("node:/fs/promises"))
|
|
368
|
+
* eq(fn("promises", "node:fs"), new URL("node:/fs/promises"))
|
|
369
|
+
* eq(fn("./promises", "node:fs"), new URL("node:/fs/promises"))
|
|
370
|
+
* eq(fn("./promises", "node:fs/"), new URL("node:/fs/promises"))
|
|
371
|
+
* eq(fn("mkdir", "node:fs/promises"), new URL("node:/fs/mkdir"))
|
|
372
|
+
* eq(fn("mkdir", "node:fs/promises/"), new URL("node:/fs/promises/mkdir"))
|
|
373
|
+
* eq(fn("./mkdir", "node:fs/promises"), new URL("node:/fs/mkdir"))
|
|
374
|
+
* eq(fn("./mkdir", "node:fs/promises/"), new URL("node:/fs/promises/mkdir"))
|
|
375
|
+
* eq(fn("/sync", "node:fs/promises/mkdir"), new URL("node:/fs/sync"))
|
|
376
|
+
*
|
|
377
|
+
* eq(fn("npm:react"), new URL("npm:/react/"))
|
|
378
|
+
* eq(fn("npm:react/file.txt"), new URL("npm:/react/file.txt"))
|
|
379
|
+
* eq(fn("npm:@facebook/react"), new URL("npm:/@facebook/react/"))
|
|
380
|
+
* eq(fn("./to/file.txt", "npm:react"), new URL("npm:/react/to/file.txt"))
|
|
381
|
+
* eq(fn("./to/file.txt", "npm:react/"), new URL("npm:/react/to/file.txt"))
|
|
382
|
+
* eq(fn("/to/file.txt", "npm:react/native/bin"), new URL("npm:/react/to/file.txt"))
|
|
383
|
+
* eq(fn("npm:react@19/jsx runtime.ts"), new URL("npm:/react@19/jsx%20runtime.ts"))
|
|
384
|
+
* eq(fn("npm:react@^19 <19.5/jsx.ts"), new URL("npm:/react@%5E19%20%3C19.5/jsx.ts"))
|
|
385
|
+
*
|
|
386
|
+
* eq(fn("jsr:@scope/my-lib/b.txt"), new URL("jsr:/@scope/my-lib/b.txt"))
|
|
387
|
+
* eq(fn("a/b.txt", "jsr:///@scope/my-lib"), new URL("jsr:/@scope/my-lib/a/b.txt"))
|
|
388
|
+
* eq(fn("./a/b.txt", "jsr:///@scope/my-lib"), new URL("jsr:/@scope/my-lib/a/b.txt"))
|
|
389
|
+
* eq(fn("a/b.txt", "jsr:///@scope/my-lib/c"), new URL("jsr:/@scope/my-lib/a/b.txt"))
|
|
390
|
+
* eq(fn("./a/b.txt", "jsr:///@scope/my-lib/c"), new URL("jsr:/@scope/my-lib/a/b.txt"))
|
|
391
|
+
* eq(fn("./a/b.txt", "jsr:///@scope/my-lib//c"), new URL("jsr:/@scope/my-lib/a/b.txt"))
|
|
392
|
+
* eq(fn("../a/b.txt", "jsr:/@scope/my-lib///c/"), new URL("jsr:/@scope/my-lib/a/b.txt"))
|
|
393
|
+
* eq(fn("./a/b.txt", "jsr:///@scope/my-lib/c/"), new URL("jsr:/@scope/my-lib/c/a/b.txt"))
|
|
394
|
+
* eq(fn("/a/b.txt", "jsr:my-lib/x/y/"), new URL("jsr:/my-lib/a/b.txt"))
|
|
395
|
+
* eq(fn("/a/b.txt", "jsr:@scope/my-lib/x/y/z"), new URL("jsr:/@scope/my-lib/a/b.txt"))
|
|
396
|
+
* eq(fn("/a/b.txt", "jsr:my-lib@1 || 2/x/y/z"), new URL("jsr:/my-lib@1%20%7C%7C%202/a/b.txt"))
|
|
397
|
+
* eq(fn("/a/b.txt", "jsr:@my/lib@1||2/x/y/z"), new URL("jsr:/@my/lib@1%7C%7C2/a/b.txt"))
|
|
398
|
+
*
|
|
399
|
+
* eq(fn("C:/a/b.txt", "jsr:@my/lib/x/y"), new URL("file:///C:/a/b.txt"))
|
|
400
|
+
* eq(fn("jsr:@my/lib/x/y", "C:/a/b.txt"), new URL("jsr:/@my/lib/x/y"))
|
|
401
|
+
* eq(fn("http://test.io/abc", "C:/a/b.txt"), new URL("http://test.io/abc"))
|
|
402
|
+
*
|
|
403
|
+
* eq(fn("blob:https://example.com/480-a78"), new URL("blob:https://example.com/480-a78"))
|
|
404
|
+
* eq(fn("data:text/plain;utf8,hello"), new URL("data:text/plain;utf8,hello"))
|
|
405
|
+
* eq(fn("data:text/plain;utf8,hello", "C:/a/b/"), new URL("data:text/plain;utf8,hello"))
|
|
406
|
+
*
|
|
407
|
+
* err(() => fn("./a/b.txt", "data:text/plain;charset=utf-8;base64,aGVsbG8="))
|
|
408
|
+
* err(() => fn("./a/b.txt", "blob:https://example.com/4800d2d8-a78c-4895-b68b-3690b69a0d6a"))
|
|
409
|
+
* err(() => fn("./a/b.txt", "./path/")) // a base path must not be relative
|
|
410
|
+
* err(() => fn("./a/b.txt")) // a relative path cannot be resolved on its own without a base path
|
|
411
|
+
* err(() => fn("./a/b.txt", "")) // an empty base path is as good as a non-existing one
|
|
412
|
+
* err(() => fn("fs/promises", "node:")) // the base protocol ("node:") MUST be accompanied with a package name
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
export const resolveAsUrl = (path, base) => {
|
|
416
|
+
if (!isString(path)) {
|
|
417
|
+
return path;
|
|
418
|
+
}
|
|
419
|
+
path = pathToPosixPath(path);
|
|
420
|
+
let base_url = base;
|
|
421
|
+
if (isString(base) && base !== "") {
|
|
422
|
+
const base_scheme = getUriScheme(base);
|
|
423
|
+
if (forbiddenBaseUriSchemes.includes(base_scheme)) {
|
|
424
|
+
throw new Error(DEBUG.ERROR ? ("the following base scheme (url-protocol) is not supported: " + base_scheme) : "");
|
|
425
|
+
}
|
|
426
|
+
base_url = resolveAsUrl(base);
|
|
427
|
+
}
|
|
428
|
+
const path_scheme = getUriScheme(path), base_protocol = base_url ? base_url.protocol : undefined, path_is_package = packageUriSchemes.includes(path_scheme), base_is_package = packageUriProtocols.includes(base_protocol), path_is_root = string_starts_with(path, "/"), path_is_local = path_scheme === "local", path_is_relative = path_scheme === "relative";
|
|
429
|
+
// handling cases like: `fn("jsr://@hello/world/c/d.txt", "jsr://@scope/lib/a/b.ts") => "jsr://@hello/world/c/d.txt"`
|
|
430
|
+
if (path_is_package) {
|
|
431
|
+
// if the `path`'s protocol scheme is that of a package (i.e. "jsr", "npm", or "node"), then we're going to it handle slightly differently,
|
|
432
|
+
// since it is possible for it to be non-parsable by the `URL` constructor if there is not trailing slash after the "npm:" or "jsr:" protocol.
|
|
433
|
+
// thus we normalize our `path` by passing it to the `parsePackageUrl` function, and acquiring the normalized `URL` compatible `href` representation of the full `path`.
|
|
434
|
+
return new URL(parsePackageUrl(path).href);
|
|
435
|
+
}
|
|
436
|
+
// if the base protocol's scheme is either "jsr" or "npm", then we're going to handle slightly differently, since it is possible for it to be non-parsable by the `URL` constructor if there is not trailing slash after the "npm:" or "jsr:" protocol.
|
|
437
|
+
// the path joining rules of packages is different from an http url, which supports the domain name as the host. such an equivalent construction cannot be made for jsr or npm package strings.
|
|
438
|
+
if (base_url && base_is_package && (path_is_root || path_is_relative)) {
|
|
439
|
+
// to start off, we parse the `protocol`, `host` (= scope + package_name + version), and any existing `pathname` of the `base_url` using the `parsePackageUrl` function.
|
|
440
|
+
// note that `pathname` always starts with a leading "/"
|
|
441
|
+
const { host, protocol, pathname } = parsePackageUrl(base_url);
|
|
442
|
+
// handling cases like: `fn("/c/d.txt", "jsr://@scope/lib/a/b.ts") => "jsr://@scope/lib/c/d.txt"`
|
|
443
|
+
if (path_is_root) {
|
|
444
|
+
return new URL(`${protocol}/${dom_encodeURI(host)}${dom_encodeURI(path)}`);
|
|
445
|
+
}
|
|
446
|
+
// handling cases like: `fn("../c/d.txt", "jsr://@scope/lib/a/b.ts") => "jsr://@scope/lib/a/c/d.txt"`
|
|
447
|
+
if (path_is_relative) {
|
|
448
|
+
// here, we join the pre-existing base url's `pathname` with the relative `paths`, by exploiting the URL constructor to do the joining part for us, by giving it a fake protocol named "x:".
|
|
449
|
+
const full_pathname = (new URL(path, "x:" + pathname)).pathname; // `full_pathname` is now url-encoded
|
|
450
|
+
return new URL(`${protocol}/${dom_encodeURI(host)}${full_pathname}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// handling cases like:
|
|
454
|
+
// - `fn("/c/de.txt", "https://example.com/a/b.txt") => "https://example.com/c/de.txt"`
|
|
455
|
+
// - `fn("./c/d.txt", "https://example.com/a/b.txt") => "https://example.com/a/b/c/d.txt"`
|
|
456
|
+
if (base_url && (path_is_root || path_is_relative)) {
|
|
457
|
+
return new URL(path, base_url);
|
|
458
|
+
}
|
|
459
|
+
// handling cases like:
|
|
460
|
+
// - `fn("/a/b c d.txt") => "file:///a/b%20c%20d.txt"`
|
|
461
|
+
// - `fn("C:/a/b d.txt") => "file:///C:/a/b%20d.txt"`
|
|
462
|
+
// - `fn("~/a/b/cd.txt") => "file://~/a/b%20d.txt"`
|
|
463
|
+
if (path_is_local) {
|
|
464
|
+
return new URL("file://" + dom_encodeURI(path));
|
|
465
|
+
}
|
|
466
|
+
// handling all other situations with absolute `path`, such as `http://`, `file://`, `blob:`, and `data:`
|
|
467
|
+
return new URL(path);
|
|
468
|
+
};
|
|
469
|
+
/** trim the leading forward-slashes at the beginning of a string.
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* ```ts
|
|
473
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
474
|
+
*
|
|
475
|
+
* // aliasing our functions for brevity
|
|
476
|
+
* const eq = assertEquals, fn = trimStartSlashes
|
|
477
|
+
*
|
|
478
|
+
* eq(fn("///a/b.txt//"), "a/b.txt//")
|
|
479
|
+
* eq(fn("/.//a/b.txt//"), ".//a/b.txt//")
|
|
480
|
+
* eq(fn(".///../a/b.txt//"), ".///../a/b.txt//")
|
|
481
|
+
* eq(fn("file:///a/b.txt//"), "file:///a/b.txt//")
|
|
482
|
+
* ```
|
|
483
|
+
*/
|
|
484
|
+
export const trimStartSlashes = (str) => {
|
|
485
|
+
return str.replace(leading_slashes_regex, "");
|
|
486
|
+
};
|
|
487
|
+
/** trim the trailing forward-slashes at the end of a string, except for those that are preceded by a dotslash ("/./") or a dotdotslash ("/../")
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```ts
|
|
491
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
492
|
+
*
|
|
493
|
+
* // aliasing our functions for brevity
|
|
494
|
+
* const eq = assertEquals, fn = trimEndSlashes
|
|
495
|
+
*
|
|
496
|
+
* eq(fn("///a/b.zip///"), "///a/b.zip")
|
|
497
|
+
* eq(fn("///a/b.zip/.///"), "///a/b.zip/./")
|
|
498
|
+
* eq(fn("///a/b.zip/..///"), "///a/b.zip/../")
|
|
499
|
+
* eq(fn("///a/b.zip/...///"), "///a/b.zip/...")
|
|
500
|
+
* eq(fn("///a/b.zip/wut.///"), "///a/b.zip/wut.")
|
|
501
|
+
* eq(fn("///a/b.zip/wut..///"), "///a/b.zip/wut..")
|
|
502
|
+
* eq(fn("///a/b.zip/wut...///"), "///a/b.zip/wut...")
|
|
503
|
+
* eq(fn(".///../a/b.zip/"), ".///../a/b.zip")
|
|
504
|
+
* eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt")
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
export const trimEndSlashes = (str) => {
|
|
508
|
+
return str.replace(trailing_slashes_regex, "");
|
|
509
|
+
};
|
|
510
|
+
/** trim leading and trailing forward-slashes, at the beginning and end of a string.
|
|
511
|
+
* this is a combination of {@link trimStartSlashes} and {@link trimEndSlashes}, so see their doc comments for more precise test cases.
|
|
512
|
+
*
|
|
513
|
+
* @example
|
|
514
|
+
* ```ts
|
|
515
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
516
|
+
*
|
|
517
|
+
* // aliasing our functions for brevity
|
|
518
|
+
* const eq = assertEquals, fn = trimSlashes
|
|
519
|
+
*
|
|
520
|
+
* eq(fn("///a/b.zip//"), "a/b.zip")
|
|
521
|
+
* eq(fn("///a/b.zip/..///"), "a/b.zip/../")
|
|
522
|
+
* eq(fn("///a/b.zip/...///"), "a/b.zip/...")
|
|
523
|
+
* eq(fn("///a/b.zip/.//..///"), "a/b.zip/.//../")
|
|
524
|
+
* eq(fn("///a/b.zip/.//.///"), "a/b.zip/.//./")
|
|
525
|
+
* eq(fn(".///../a/b.zip//"), ".///../a/b.zip")
|
|
526
|
+
* eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt")
|
|
527
|
+
* ```
|
|
528
|
+
*/
|
|
529
|
+
export const trimSlashes = (str) => {
|
|
530
|
+
return trimEndSlashes(trimStartSlashes(str));
|
|
531
|
+
};
|
|
532
|
+
/** ensure that there is at least one leading slash at the beginning.
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```ts
|
|
536
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
537
|
+
*
|
|
538
|
+
* // aliasing our functions for brevity
|
|
539
|
+
* const eq = assertEquals, fn = ensureStartSlash
|
|
540
|
+
*
|
|
541
|
+
* eq(fn("a/b.zip"), "/a/b.zip")
|
|
542
|
+
* eq(fn(".///../a/b.zip/"), "/.///../a/b.zip/")
|
|
543
|
+
* eq(fn("///../a/b.zip/"), "///../a/b.zip/")
|
|
544
|
+
* eq(fn("file:///a/b.zip//c.txt"), "/file:///a/b.zip//c.txt")
|
|
545
|
+
* ```
|
|
546
|
+
*/
|
|
547
|
+
export const ensureStartSlash = (str) => {
|
|
548
|
+
return string_starts_with(str, sep) ? str : sep + str;
|
|
549
|
+
};
|
|
550
|
+
/** ensure that there is at least one leading dot-slash at the beginning.
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* ```ts
|
|
554
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
555
|
+
*
|
|
556
|
+
* // aliasing our functions for brevity
|
|
557
|
+
* const eq = assertEquals, fn = ensureStartDotSlash
|
|
558
|
+
*
|
|
559
|
+
* eq(fn("a/b.zip"), "./a/b.zip")
|
|
560
|
+
* eq(fn(".///../a/b.zip/"), ".///../a/b.zip/")
|
|
561
|
+
* eq(fn("///../a/b.zip/"), ".///../a/b.zip/")
|
|
562
|
+
* eq(fn("file:///a/b.zip//c.txt"), "./file:///a/b.zip//c.txt")
|
|
563
|
+
* ```
|
|
564
|
+
*/
|
|
565
|
+
export const ensureStartDotSlash = (str) => {
|
|
566
|
+
return string_starts_with(str, dotslash) ? str
|
|
567
|
+
: string_starts_with(str, sep) ? "." + str
|
|
568
|
+
: dotslash + str;
|
|
569
|
+
};
|
|
570
|
+
/** ensure that there is at least one trailing slash at the end.
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* ```ts
|
|
574
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
575
|
+
*
|
|
576
|
+
* // aliasing our functions for brevity
|
|
577
|
+
* const eq = assertEquals, fn = ensureEndSlash
|
|
578
|
+
*
|
|
579
|
+
* eq(fn("///a/b.zip//"), "///a/b.zip//")
|
|
580
|
+
* eq(fn(".///../a/b.zip/"), ".///../a/b.zip/")
|
|
581
|
+
* eq(fn(".///../a/b.zip/."), ".///../a/b.zip/./")
|
|
582
|
+
* eq(fn(".///../a/b.zip/./"), ".///../a/b.zip/./")
|
|
583
|
+
* eq(fn(".///../a/b.zip/.."), ".///../a/b.zip/../")
|
|
584
|
+
* eq(fn(".///../a/b.zip/../"), ".///../a/b.zip/../")
|
|
585
|
+
* eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt/")
|
|
586
|
+
* ```
|
|
587
|
+
*/
|
|
588
|
+
export const ensureEndSlash = (str) => {
|
|
589
|
+
return string_ends_with(str, sep) ? str : str + sep;
|
|
590
|
+
};
|
|
591
|
+
/** trim leading forward-slashes ("/") and dot-slashes ("./"), at the beginning a string.
|
|
592
|
+
* but exclude non-trivial dotdotslash ("/../") from being wrongfully trimmed.
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* ```ts
|
|
596
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
597
|
+
*
|
|
598
|
+
* // aliasing our functions for brevity
|
|
599
|
+
* const eq = assertEquals, fn = trimStartDotSlashes
|
|
600
|
+
*
|
|
601
|
+
* eq(fn("///a/b.zip/c.txt"), "a/b.zip/c.txt")
|
|
602
|
+
* eq(fn("///a/b.zip//"), "a/b.zip//")
|
|
603
|
+
* eq(fn("//..//a/b.zip//"), "..//a/b.zip//")
|
|
604
|
+
* eq(fn("/./..//a/b.zip//"), "..//a/b.zip//")
|
|
605
|
+
* eq(fn("/./.././a/b.zip//"), ".././a/b.zip//")
|
|
606
|
+
* eq(fn("///././///.////a/b.zip//"), "a/b.zip//")
|
|
607
|
+
* eq(fn(".///././///.////a/b.zip//"), "a/b.zip//")
|
|
608
|
+
* eq(fn("./././//././///.////a/b.zip//"), "a/b.zip//")
|
|
609
|
+
* eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt")
|
|
610
|
+
* ```
|
|
611
|
+
*/
|
|
612
|
+
export const trimStartDotSlashes = (str) => {
|
|
613
|
+
return str.replace(leading_slashes_and_dot_slashes_regex, "");
|
|
614
|
+
};
|
|
615
|
+
/** trim all trivial trailing forward-slashes ("/") and dot-slashes ("./"), at the end a string.
|
|
616
|
+
* but exclude non-trivial dotdotslash ("/../") from being wrongfully trimmed.
|
|
617
|
+
*
|
|
618
|
+
* TODO: this operation is somewhat expensive, because:
|
|
619
|
+
* - the implementation uses regex, however it was not possible for me to design a regex that handles the input string as is,
|
|
620
|
+
* so I resort to reversing the input string, and using a slightly easier-to-design regex that discovers trivial (dot)slashes in reverse order,
|
|
621
|
+
* and then after the string replacement, I reverse it again and return it as the output.
|
|
622
|
+
*
|
|
623
|
+
* @example
|
|
624
|
+
* ```ts
|
|
625
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
626
|
+
*
|
|
627
|
+
* // aliasing our functions for brevity
|
|
628
|
+
* const eq = assertEquals, fn = trimEndDotSlashes
|
|
629
|
+
*
|
|
630
|
+
* eq(fn("a/b.zip/c.txt"), "a/b.zip/c.txt")
|
|
631
|
+
* eq(fn("//a/b.zip//"), "//a/b.zip")
|
|
632
|
+
* eq(fn("/"), "")
|
|
633
|
+
* eq(fn("./"), "")
|
|
634
|
+
* eq(fn("//././//./"), "")
|
|
635
|
+
* eq(fn(".//././//./"), "")
|
|
636
|
+
* eq(fn(".//./..///./"), ".//./../")
|
|
637
|
+
* eq(fn("/a/b.zip/./"), "/a/b.zip")
|
|
638
|
+
* eq(fn("/a/b.zip/../"), "/a/b.zip/../")
|
|
639
|
+
* eq(fn("/a/b.zip/..//"), "/a/b.zip/../")
|
|
640
|
+
* eq(fn("/a/b.zip/.././"), "/a/b.zip/../")
|
|
641
|
+
* eq(fn("a/b.zip///././///.////"), "a/b.zip")
|
|
642
|
+
* eq(fn("/a/b.zip///././///.////"), "/a/b.zip")
|
|
643
|
+
* eq(fn("/a/b.zip/.././/.././///.////"), "/a/b.zip/.././/../")
|
|
644
|
+
* eq(fn("/a/b.zip/././././///.////"), "/a/b.zip")
|
|
645
|
+
* eq(fn("/a/b.zip./././././///.////"), "/a/b.zip.")
|
|
646
|
+
* eq(fn("/a/b.zip../././././///.////"), "/a/b.zip..")
|
|
647
|
+
* eq(fn("/a/b.zip.../././././///.////"), "/a/b.zip...")
|
|
648
|
+
* eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt")
|
|
649
|
+
* ```
|
|
650
|
+
*/
|
|
651
|
+
export const trimEndDotSlashes = (str) => {
|
|
652
|
+
const reversed_str = [...str].toReversed().join(""), trimmed_reversed_str = reversed_str.replace(reversed_trailing_slashes_and_dot_slashes_regex, "");
|
|
653
|
+
// there is a special case when the entirety of the original `str` is made up of only (dot)slashes and ends with one dotslashes "./" (trailing relative path navigation),
|
|
654
|
+
// in which case, we will be left with one single "." as our `trimmed_reversed_str`, instead of an empty string.
|
|
655
|
+
// so we handle this special case below, otherwise we process all other `trimmed_reversed_str` by reversing it once more.
|
|
656
|
+
return trimmed_reversed_str === "."
|
|
657
|
+
? ""
|
|
658
|
+
: [...trimmed_reversed_str].toReversed().join("");
|
|
659
|
+
};
|
|
660
|
+
/** trim leading and trailing forward-slashes ("/") and dot-slashes ("./"), at the beginning and end of a string, but keep trailing non-trivial ones intact.
|
|
661
|
+
* this is a combination of {@link trimStartDotSlashes} and {@link trimEndDotSlashes}, so see their doc comments for more precise test cases.
|
|
662
|
+
*
|
|
663
|
+
* @example
|
|
664
|
+
* ```ts
|
|
665
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
666
|
+
*
|
|
667
|
+
* // aliasing our functions for brevity
|
|
668
|
+
* const eq = assertEquals, fn = trimDotSlashes
|
|
669
|
+
*
|
|
670
|
+
* eq(fn("///a/b.zip//"), "a/b.zip")
|
|
671
|
+
* eq(fn(".///../a/b.zip//"), "../a/b.zip")
|
|
672
|
+
* eq(fn("//./././///././//../a/b.zip//"), "../a/b.zip")
|
|
673
|
+
* eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt")
|
|
674
|
+
* ```
|
|
675
|
+
*/
|
|
676
|
+
export const trimDotSlashes = (str) => {
|
|
677
|
+
return trimEndDotSlashes(trimStartDotSlashes(str));
|
|
678
|
+
};
|
|
679
|
+
/** TODO: purge this function in the future, if you absolutely do not use it anywhere.
|
|
680
|
+
* @deprecated
|
|
681
|
+
*
|
|
682
|
+
* > [!note]
|
|
683
|
+
* > you'd probably want to use {@link joinPaths} instead of this function, for any realistic set of path segments.
|
|
684
|
+
* > not only is this more expensive to compute, it does not distinguish between a directory and a file path (intentionally).
|
|
685
|
+
*
|
|
686
|
+
* join path segments with forward-slashes in between, and remove redundant slashes ("/") and dotslashes ("./") around each segment (if any).
|
|
687
|
+
* however, the first segment's leading and trailing slashes are left untouched,
|
|
688
|
+
* because that would potentially strip away location information (such as relative path ("./"), or absolute path ("/"), or some uri ("file:///")).
|
|
689
|
+
*
|
|
690
|
+
* if you want to ensure that your first segment is shortened, use either the {@link normalizePath} or {@link normalizePosixPath} function on it before passing it here.
|
|
691
|
+
*
|
|
692
|
+
* > [!warning]
|
|
693
|
+
* > it is recommended that you use segments with posix path dir-separators ("/").
|
|
694
|
+
*
|
|
695
|
+
* @example
|
|
696
|
+
* ```ts
|
|
697
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
698
|
+
*
|
|
699
|
+
* // aliasing our functions for brevity
|
|
700
|
+
* const eq = assertEquals, fn = joinSlash
|
|
701
|
+
*
|
|
702
|
+
* eq(fn(".///../a", "b", "c.txt"), ".///../a/b/c.txt")
|
|
703
|
+
* eq(fn("file:///a/", "b.zip//", "./c.txt"), "file:///a/b.zip/c.txt")
|
|
704
|
+
* eq(fn("file:///", "a/", "b.zip//", "./c.txt"), "file:///a/b.zip/c.txt")
|
|
705
|
+
* eq(fn("///a//", "b.//", "zip..//"), "///a//b./zip..")
|
|
706
|
+
* eq(fn("a/", "b.zip", "./c.txt", ""), "a/b.zip/c.txt/")
|
|
707
|
+
* eq(fn("a/", "b.zip", "./c.txt", "."), "a/b.zip/c.txt/")
|
|
708
|
+
* eq(fn("a/", "b.zip", "./c.txt", "./"), "a/b.zip/c.txt/")
|
|
709
|
+
* eq(fn("a/", "b.zip", "./c.txt", ".."), "a/b.zip/c.txt/..")
|
|
710
|
+
* eq(fn("a/", "b.zip", "./c.txt", "..."), "a/b.zip/c.txt/...")
|
|
711
|
+
* eq(fn("", "", ""), "")
|
|
712
|
+
* eq(fn("/", "", ""), "/")
|
|
713
|
+
* eq(fn("/", "/", ""), "/")
|
|
714
|
+
* eq(fn("/", "", "/"), "/")
|
|
715
|
+
* eq(fn("/", "/", "/"), "/")
|
|
716
|
+
* eq(fn("./", "", ""), "./")
|
|
717
|
+
* eq(fn("./", "./", ""), "./")
|
|
718
|
+
* eq(fn("./", "", "./"), "./")
|
|
719
|
+
* eq(fn("./", "./", "./"), "./")
|
|
720
|
+
* eq(fn(
|
|
721
|
+
* "//./././///././//../a/b.zip/.////",
|
|
722
|
+
* "///.////././.././c.txt/./../",
|
|
723
|
+
* "../../d.xyz//.//",
|
|
724
|
+
* ), "//./././///././//../a/b.zip/.////.././c.txt/./../../../d.xyz")
|
|
725
|
+
* ```
|
|
726
|
+
*/
|
|
727
|
+
export const joinSlash = (first_segment = "", ...segments) => {
|
|
728
|
+
return segments
|
|
729
|
+
.map(trimDotSlashes)
|
|
730
|
+
.reduce((output, subpath) => ((output === "" ? "" : ensureEndSlash(output)) + subpath), first_segment);
|
|
731
|
+
};
|
|
732
|
+
/** normalize a path by reducing and removing redundant dot-slash ("./" and "../") path navigators from a path.
|
|
733
|
+
*
|
|
734
|
+
* if you provide the optional `config` with the `keepRelative` set to `false`, then in the output, there will no be leading dot-slashes ("./").
|
|
735
|
+
* read more about the option here: {@link NormalizePathConfig.keepRelative}.
|
|
736
|
+
* but note that irrespective of what you set this option to be, leading leading dotdot-slashes ("../") and leading slashes ("/") will not be trimmed.
|
|
737
|
+
*
|
|
738
|
+
* even though `config` should be of {@link NormalizePathConfig} type, it also accepts `number` so that the function's signature becomes compatible with the `Array.prototype.map` method,
|
|
739
|
+
* however, unless you pass the correct config object type, only the default action will be taken.
|
|
740
|
+
*
|
|
741
|
+
* > [!warning]
|
|
742
|
+
* > you MUST provide a posix path (i.e. use "/" for dir-separator).
|
|
743
|
+
* > there will not be any implicit conversion of windows "\\" dir-separator.
|
|
744
|
+
*
|
|
745
|
+
* @example
|
|
746
|
+
* ```ts
|
|
747
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
748
|
+
*
|
|
749
|
+
* // aliasing our functions for brevity
|
|
750
|
+
* const eq = assertEquals, fn = normalizePosixPath
|
|
751
|
+
* // aliasing the config for disabling the preservation of leading "./"
|
|
752
|
+
* const remove_rel: NormalizePathConfig = { keepRelative: false }
|
|
753
|
+
*
|
|
754
|
+
* eq(fn("../a/./b/../././/c.txt"), "../a//c.txt")
|
|
755
|
+
* eq(fn("./././a/b/.././././//c.txt"), "./a///c.txt")
|
|
756
|
+
* eq(fn("./././a/b/.././././//c.txt", remove_rel), "a///c.txt")
|
|
757
|
+
* eq(fn("/a/b/.././././//c.txt"), "/a///c.txt")
|
|
758
|
+
* eq(fn("///a/b/.././././//c.txt"), "///a///c.txt")
|
|
759
|
+
* eq(fn("///a/b/.././.././//c.txt"), "/////c.txt")
|
|
760
|
+
* eq(fn("file:///./././a/b/.././././c.txt"), "file:///a/c.txt")
|
|
761
|
+
* eq(fn("/a/../"), "/")
|
|
762
|
+
* // NOTICE: the test in the next line may seem like a weird behavior.
|
|
763
|
+
* eq(fn("/a/../../"), "")
|
|
764
|
+
* eq(fn("/a/../../../"), "../")
|
|
765
|
+
* eq(fn("./a/../../"), "../")
|
|
766
|
+
* eq(fn("./a/../../../"), "../../")
|
|
767
|
+
* eq(fn("/a/b/../.."), "/")
|
|
768
|
+
* eq(fn("/a/b/."), "/a/b/")
|
|
769
|
+
* eq(fn("/a/b/./"), "/a/b/")
|
|
770
|
+
* eq(fn("/a/b/c/.."), "/a/b/")
|
|
771
|
+
* eq(fn("/a/b/c/../."), "/a/b/")
|
|
772
|
+
* eq(fn("/a/b/c/d/../.."), "/a/b/")
|
|
773
|
+
* eq(fn("/a/b/c/../.nomedia"), "/a/b/.nomedia")
|
|
774
|
+
* eq(fn(""), "")
|
|
775
|
+
* eq(fn("."), ".")
|
|
776
|
+
* eq(fn(".."), "..")
|
|
777
|
+
* eq(fn("./"), "./")
|
|
778
|
+
* eq(fn("../"), "../")
|
|
779
|
+
* eq(fn("../."), "../")
|
|
780
|
+
* eq(fn("../.."), "../../")
|
|
781
|
+
* eq(fn("./././././"), "./")
|
|
782
|
+
* eq(fn(".././././"), "../")
|
|
783
|
+
* eq(fn("./././.././././"), "../")
|
|
784
|
+
* eq(fn("./././.././.././"), "../../")
|
|
785
|
+
* eq(fn(".", remove_rel), "")
|
|
786
|
+
* eq(fn("./", remove_rel), "")
|
|
787
|
+
* eq(fn("./././././", remove_rel), "")
|
|
788
|
+
* eq(fn("./././.././././", remove_rel), "../")
|
|
789
|
+
* eq(fn("./././.././.././", remove_rel), "../../")
|
|
790
|
+
* ```
|
|
791
|
+
*/
|
|
792
|
+
export const normalizePosixPath = (path, config = {}) => {
|
|
793
|
+
const { keepRelative = true } = isObject(config) ? config : {}, segments = path.split(sep), last_segment = segments.at(-1), output_segments = [".."],
|
|
794
|
+
// a flag that specifies whether a "./" should be prepended to final result (assuming that the result does not being with "../")
|
|
795
|
+
prepend_relative_dotslash_to_output_segments = keepRelative && segments[0] === ".",
|
|
796
|
+
// a flag that specifies whether the input path ends with a "/." or "/..", in which case we will need to append a final "/" to the output.
|
|
797
|
+
// this is because our for-loop will strip away the final dot character, without any upcoming replacements.
|
|
798
|
+
// for instance, an input `path = "/a/b/."` would normalize to `"/a/b/"` with this flag. but without it, it will become `"/a/b"`.
|
|
799
|
+
ends_with_dir_navigator_without_a_trailing_slash = (segments.length >= 2) && (last_segment === "." || last_segment === "..");
|
|
800
|
+
if (ends_with_dir_navigator_without_a_trailing_slash) {
|
|
801
|
+
segments.push("");
|
|
802
|
+
}
|
|
803
|
+
for (const segment of segments) {
|
|
804
|
+
if (segment === "..") {
|
|
805
|
+
if (output_segments.at(-1) !== "..") {
|
|
806
|
+
output_segments.pop();
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
output_segments.push(segment);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
else if (segment !== ".") {
|
|
813
|
+
output_segments.push(segment);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
output_segments.shift();
|
|
817
|
+
if (prepend_relative_dotslash_to_output_segments && output_segments[0] !== "..") {
|
|
818
|
+
output_segments.unshift(".");
|
|
819
|
+
}
|
|
820
|
+
return output_segments.join(sep);
|
|
821
|
+
};
|
|
822
|
+
/** normalize a path by reducing and removing redundant dot-slash ("./", "../", ".\\", and "..\\") path navigators from a path.
|
|
823
|
+
* the returned output is always a posix-style path.
|
|
824
|
+
*
|
|
825
|
+
* to read about the optional `config` parameter, refer to the docs of {@link normalizePosixPath}, which is the underlying function that takes care most of the normalization.
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* ```ts
|
|
829
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
830
|
+
*
|
|
831
|
+
* // aliasing our functions for brevity
|
|
832
|
+
* const eq = assertEquals, fn = normalizePath
|
|
833
|
+
*
|
|
834
|
+
* eq(fn("../a/./b/../././/c.txt"), "../a//c.txt")
|
|
835
|
+
* eq(fn("./.\\.\\a\\b\\.././.\\.///c.txt"), "./a///c.txt")
|
|
836
|
+
* eq(fn("/home\\.config/a\\..\\...\\b\\./c.txt"), "/home/.config/.../b/c.txt")
|
|
837
|
+
* eq(fn("file:///./././a\\b/..\\././.\\c.txt"), "file:///a/c.txt")
|
|
838
|
+
* ```
|
|
839
|
+
*/
|
|
840
|
+
export const normalizePath = (path, config) => {
|
|
841
|
+
return normalizePosixPath(pathToPosixPath(path), config);
|
|
842
|
+
};
|
|
843
|
+
/** convert windows directory slash "\\" to posix directory slash "/".
|
|
844
|
+
*
|
|
845
|
+
* @example
|
|
846
|
+
* ```ts
|
|
847
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
848
|
+
*
|
|
849
|
+
* // aliasing our functions for brevity
|
|
850
|
+
* const eq = assertEquals, fn = pathToPosixPath
|
|
851
|
+
*
|
|
852
|
+
* eq(fn("C:\\Users/my name\\file.txt"), "C:/Users/my name/file.txt")
|
|
853
|
+
* eq(fn("~/path/to/file.txt"), "~/path/to/file.txt")
|
|
854
|
+
* eq(fn("/path\\to file.txt"), "/path/to file.txt")
|
|
855
|
+
* ```
|
|
856
|
+
*/
|
|
857
|
+
export const pathToPosixPath = (path) => path.replaceAll(windows_directory_slash_regex, sep);
|
|
858
|
+
/** convert an array of paths to cli compatible list of paths, suitable for setting as an environment variable.
|
|
859
|
+
*
|
|
860
|
+
* @example
|
|
861
|
+
* ```ts
|
|
862
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
863
|
+
*
|
|
864
|
+
* // aliasing our functions for brevity
|
|
865
|
+
* const eq = assertEquals, fn = pathsToCliArg
|
|
866
|
+
*
|
|
867
|
+
* // conversion example with windows separator (";")
|
|
868
|
+
* eq(fn(";", [
|
|
869
|
+
* "./a/b/c.txt",
|
|
870
|
+
* "C:\\Android Studio\\sdk\\",
|
|
871
|
+
* "build\\libs\\"
|
|
872
|
+
* ]), `"./a/b/c.txt;C:/Android Studio/sdk/;build/libs/"`)
|
|
873
|
+
*
|
|
874
|
+
* // conversion example with unix separator (":")
|
|
875
|
+
* eq(fn(":", [
|
|
876
|
+
* "./a/b/c.txt",
|
|
877
|
+
* "~/Android Studio/sdk/",
|
|
878
|
+
* "build/libs/"
|
|
879
|
+
* ]), `"./a/b/c.txt:~/Android Studio/sdk/:build/libs/"`)
|
|
880
|
+
* ```
|
|
881
|
+
*/
|
|
882
|
+
export const pathsToCliArg = (separator, paths) => {
|
|
883
|
+
return quote(pathToPosixPath(paths.join(separator)));
|
|
884
|
+
};
|
|
885
|
+
/** find the prefix path directory common to all provided `paths`.
|
|
886
|
+
* > [!warning]
|
|
887
|
+
* > your paths MUST be normalized beforehand, and use posix dir-separators ("/").
|
|
888
|
+
*
|
|
889
|
+
* @example
|
|
890
|
+
* ```ts
|
|
891
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
892
|
+
*
|
|
893
|
+
* // aliasing our functions for brevity
|
|
894
|
+
* const eq = assertEquals, fn = commonNormalizedPosixPath
|
|
895
|
+
*
|
|
896
|
+
* eq(fn([
|
|
897
|
+
* "C:/Hello/World/This/Is/An/Example/Bla.cs",
|
|
898
|
+
* "C:/Hello/World/This/Is/Not/An/Example/",
|
|
899
|
+
* "C:/Hello/Earth/Bla/Bla/Bla",
|
|
900
|
+
* ]), "C:/Hello/")
|
|
901
|
+
*
|
|
902
|
+
* eq(fn([
|
|
903
|
+
* "C:/Hello/World/This/Is/An/Example/Bla.cs",
|
|
904
|
+
* "C:/Hello/World/This/is/an/example/bla.cs",
|
|
905
|
+
* "C:/Hello/World/This/Is/Not/An/Example/",
|
|
906
|
+
* ]), "C:/Hello/World/This/")
|
|
907
|
+
*
|
|
908
|
+
* eq(fn([
|
|
909
|
+
* "./../Hello/World/Users/This/Is/An/Example/Bla.cs",
|
|
910
|
+
* "./../Hello/World Users/This/Is/An/example/bla.cs",
|
|
911
|
+
* "./../Hello/World-Users/This/Is/Not/An/Example/",
|
|
912
|
+
* ]), "./../Hello/")
|
|
913
|
+
*
|
|
914
|
+
* eq(fn([
|
|
915
|
+
* "./Hello/World/Users/This/Is/An/Example/Bla.cs",
|
|
916
|
+
* "./Hello/World/",
|
|
917
|
+
* "./Hello/World", // the "World" here segment is not treated as a directory
|
|
918
|
+
* ]), "./Hello/")
|
|
919
|
+
*
|
|
920
|
+
* eq(fn([
|
|
921
|
+
* "C:/Hello/World/",
|
|
922
|
+
* "/C:/Hello/World/",
|
|
923
|
+
* "C:/Hello/World/",
|
|
924
|
+
* ]), "") // no common prefix was identified
|
|
925
|
+
* ```
|
|
926
|
+
*/
|
|
927
|
+
export const commonNormalizedPosixPath = (paths) => {
|
|
928
|
+
const common_prefix = commonPrefix(paths), common_prefix_length = common_prefix.length;
|
|
929
|
+
for (const path of paths) {
|
|
930
|
+
const remaining_substring = path.slice(common_prefix_length);
|
|
931
|
+
if (!string_starts_with(remaining_substring, sep)) {
|
|
932
|
+
// it looks like the `path`'s common prefix is not followed by an immediate "/" separator.
|
|
933
|
+
// thus, we must now reduce our `common_prefix` to the last available "/" separator.
|
|
934
|
+
// after we do that, we are guaranteed that this newly created `common_dir_prefix` is indeed common to all `paths`, since its superset, the `common_prefix`, was also common to all `paths`.
|
|
935
|
+
// thus we can immediately return and ignore the remaining tests in the loop.
|
|
936
|
+
const common_dir_prefix_length = common_prefix.lastIndexOf(sep) + 1, common_dir_prefix = common_prefix.slice(0, common_dir_prefix_length);
|
|
937
|
+
return common_dir_prefix;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// if we have made it to here, it would mean that among all paths, the initial `common_prefix` was indeed also the common directory among all of them.
|
|
941
|
+
return common_prefix;
|
|
942
|
+
};
|
|
943
|
+
/** find the prefix path directory common to all provided `paths`.
|
|
944
|
+
* your input `paths` do not need to be normalized nor necessarily use posix-style separator "/".
|
|
945
|
+
* under the hood, this function normalizes and converts all paths to posix-style, then applies the {@link commonNormalizedPosixPath} onto them.
|
|
946
|
+
*
|
|
947
|
+
* @example
|
|
948
|
+
* ```ts
|
|
949
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
950
|
+
*
|
|
951
|
+
* // aliasing our functions for brevity
|
|
952
|
+
* const eq = assertEquals, fn = commonPath
|
|
953
|
+
*
|
|
954
|
+
* eq(fn([
|
|
955
|
+
* "C:/Hello/World/This/Is/An/Example/Bla.cs",
|
|
956
|
+
* "C:\\Hello\\World\\This\\Is\\Not/An/Example/",
|
|
957
|
+
* "C:/Hello/Earth/Bla/Bla/Bla",
|
|
958
|
+
* ]), "C:/Hello/")
|
|
959
|
+
*
|
|
960
|
+
* eq(fn([
|
|
961
|
+
* "./Hello/World/This/Used/to-be-an/example/../../../Is/An/Example/Bla.cs",
|
|
962
|
+
* ".\\Hello/World/This/Is/an/example/bla.cs",
|
|
963
|
+
* "./Hello/World/This/Is/Not/An/Example/",
|
|
964
|
+
* ]), "./Hello/World/This/Is/")
|
|
965
|
+
*
|
|
966
|
+
* eq(fn([
|
|
967
|
+
* "./../home/Hello/World/Users/This/Is/An/Example/Bla.cs",
|
|
968
|
+
* "././../home\\Hello\\World Users\\This\\Is/An\\example/bla.cs",
|
|
969
|
+
* "./../home/./.\\.\\././Hello/World-Users/./././././This/Is/Not/An/Example/",
|
|
970
|
+
* ]), "../home/Hello/")
|
|
971
|
+
*
|
|
972
|
+
* eq(fn([
|
|
973
|
+
* "\\C:/Hello/World/Users/This/Is/An/Example/Bla.cs",
|
|
974
|
+
* "/C:\\Hello\\World Users\\This\\Is/An\\example/bla.cs",
|
|
975
|
+
* "/C:/Hello/World", // the "World" here segment is not treated as a directory
|
|
976
|
+
* ]), "/C:/Hello/")
|
|
977
|
+
* ```
|
|
978
|
+
*/
|
|
979
|
+
export const commonPath = (paths) => {
|
|
980
|
+
return commonNormalizedPosixPath(paths.map(normalizePath));
|
|
981
|
+
};
|
|
982
|
+
/** replace the common path among all provided `paths` by transforming it with a custom `map_fn` function.
|
|
983
|
+
* all `paths` are initially normalized and converted into posix-style (so that no "\\" windows separator is prevelent).
|
|
984
|
+
*
|
|
985
|
+
* the `map_fn` function's first argument (`path_info`), is a 2-tuple of the form `[common_dir: string, subpath: string]`,
|
|
986
|
+
* where `common_dir` represents the directory common to all of the input `paths`, and the `subpath` represents the remaining relative path that comes after common_dir.
|
|
987
|
+
* - the `common_dir` always ends with a trailing slash ("/"), unless there is absolutely no common directory among the `paths` at all.
|
|
988
|
+
* - the `subpath` never begins with any slash (nor any dot-slashes), unless of course, you had initially provided a path containing two or more consecutive slashes.
|
|
989
|
+
*
|
|
990
|
+
* @example
|
|
991
|
+
* ```ts
|
|
992
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
993
|
+
*
|
|
994
|
+
* // aliasing our functions for brevity
|
|
995
|
+
* const eq = assertEquals, fn = commonPathTransform
|
|
996
|
+
*
|
|
997
|
+
* const subpath_map_fn = ([common_dir, subpath]: [string, string]) => (subpath)
|
|
998
|
+
*
|
|
999
|
+
* eq(fn([
|
|
1000
|
+
* "C:/Hello/World/This/Is/An/Example/Bla.cs",
|
|
1001
|
+
* "C:\\Hello\\World\\This\\Is\\Not/An/Example/",
|
|
1002
|
+
* "C:/Hello/Earth/Bla/Bla/Bla",
|
|
1003
|
+
* ], subpath_map_fn), [
|
|
1004
|
+
* "World/This/Is/An/Example/Bla.cs",
|
|
1005
|
+
* "World/This/Is/Not/An/Example/",
|
|
1006
|
+
* "Earth/Bla/Bla/Bla",
|
|
1007
|
+
* ])
|
|
1008
|
+
*
|
|
1009
|
+
* eq(fn([
|
|
1010
|
+
* "./../././home/Hello/World/This/Used/to-be-an/example/../../../Is/An/Example/Bla.cs",
|
|
1011
|
+
* "./././../home/Hello/World/This/Is/an/example/bla.cs",
|
|
1012
|
+
* "./../home/Hello/World/This/Is/Not/An/Example/",
|
|
1013
|
+
* ], subpath_map_fn), [
|
|
1014
|
+
* "An/Example/Bla.cs",
|
|
1015
|
+
* "an/example/bla.cs",
|
|
1016
|
+
* "Not/An/Example/",
|
|
1017
|
+
* ])
|
|
1018
|
+
*
|
|
1019
|
+
* eq(fn([
|
|
1020
|
+
* "/C:/Hello///World/Users/This/Is/An/Example/Bla.cs",
|
|
1021
|
+
* "/C:\\Hello\\World Users\\This\\Is/An\\example/bla.cs",
|
|
1022
|
+
* "/C:/./.\\.\\././Hello/World-Users/./././././This/Is/Not/An/Example/",
|
|
1023
|
+
* ], subpath_map_fn), [
|
|
1024
|
+
* "//World/Users/This/Is/An/Example/Bla.cs",
|
|
1025
|
+
* "World Users/This/Is/An/example/bla.cs",
|
|
1026
|
+
* "World-Users/This/Is/Not/An/Example/",
|
|
1027
|
+
* ])
|
|
1028
|
+
* ```
|
|
1029
|
+
*/
|
|
1030
|
+
export const commonPathTransform = (paths, map_fn) => {
|
|
1031
|
+
const normal_paths = paths.map(normalizePath), common_dir = commonNormalizedPosixPath(normal_paths), common_dir_length = common_dir.length, path_infos = array_from(normal_paths, (normal_path) => {
|
|
1032
|
+
return [common_dir, normal_path.slice(common_dir_length)];
|
|
1033
|
+
});
|
|
1034
|
+
return path_infos.map(map_fn);
|
|
1035
|
+
};
|
|
1036
|
+
/** purge the common path among all provided `paths`, and replace (join) it with a `new_common_dir` path.
|
|
1037
|
+
*
|
|
1038
|
+
* @example
|
|
1039
|
+
* ```ts
|
|
1040
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
1041
|
+
*
|
|
1042
|
+
* // aliasing our functions for brevity
|
|
1043
|
+
* const eq = assertEquals, fn = commonPathReplace
|
|
1044
|
+
*
|
|
1045
|
+
* eq(fn([
|
|
1046
|
+
* "C:/Hello/World/This/Is/An/Example/Bla.cs",
|
|
1047
|
+
* "C:\\Hello\\World\\This\\Is\\Not/An/Example/",
|
|
1048
|
+
* "C:/Hello/Earth/Bla/Bla/Bla",
|
|
1049
|
+
* ], "D:/"), [
|
|
1050
|
+
* "D:/World/This/Is/An/Example/Bla.cs",
|
|
1051
|
+
* "D:/World/This/Is/Not/An/Example/",
|
|
1052
|
+
* "D:/Earth/Bla/Bla/Bla",
|
|
1053
|
+
* ])
|
|
1054
|
+
*
|
|
1055
|
+
* eq(fn([
|
|
1056
|
+
* "C:/Hello/World/This/Used/to-be-an/example/../../../Is/An/Example/Bla.cs",
|
|
1057
|
+
* "C:/Hello/World/This/Is/an/example/bla.cs",
|
|
1058
|
+
* "C:/Hello/World/This/Is/Not/An/Example/",
|
|
1059
|
+
* ], "D:/temp"), [ // an implicit forward slash is added.
|
|
1060
|
+
* "D:/temp/An/Example/Bla.cs",
|
|
1061
|
+
* "D:/temp/an/example/bla.cs",
|
|
1062
|
+
* "D:/temp/Not/An/Example/",
|
|
1063
|
+
* ])
|
|
1064
|
+
*
|
|
1065
|
+
* eq(fn([
|
|
1066
|
+
* // there is no common ancestor among each of the paths (even "C:/" and "./C:/" are not considered to be equivalent to one another)
|
|
1067
|
+
* "http:/Hello/World.cs",
|
|
1068
|
+
* "./C:/Hello/World.cs",
|
|
1069
|
+
* "C:/Hello/World/file.cs",
|
|
1070
|
+
* ], "D:/temp/"), [
|
|
1071
|
+
* "D:/temp/http:/Hello/World.cs",
|
|
1072
|
+
* "D:/temp/C:/Hello/World.cs",
|
|
1073
|
+
* "D:/temp/C:/Hello/World/file.cs",
|
|
1074
|
+
* ])
|
|
1075
|
+
*
|
|
1076
|
+
* eq(fn([
|
|
1077
|
+
* "/C:/Hello///World/Users/This/Is/An/Example/Bla.cs",
|
|
1078
|
+
* "/C:\\Hello\\World Users\\This\\Is/An\\example/bla.cs",
|
|
1079
|
+
* "/C:/./.\\.\\././Hello/World-Users/./././././This/Is/Not/An/Example/",
|
|
1080
|
+
* ], "file:///./.\\HELLO.\\./../"), [ // the `new_common_dir` is not normalized by this function
|
|
1081
|
+
* "file:///./.\\HELLO.\\./..///World/Users/This/Is/An/Example/Bla.cs",
|
|
1082
|
+
* "file:///./.\\HELLO.\\./../World Users/This/Is/An/example/bla.cs",
|
|
1083
|
+
* "file:///./.\\HELLO.\\./../World-Users/This/Is/Not/An/Example/",
|
|
1084
|
+
* ])
|
|
1085
|
+
* ```
|
|
1086
|
+
*/
|
|
1087
|
+
export const commonPathReplace = (paths, new_common_dir) => {
|
|
1088
|
+
new_common_dir = ensureEndSlash(new_common_dir);
|
|
1089
|
+
return commonPathTransform(paths, ([common_dir, subpath]) => {
|
|
1090
|
+
// if there is no common dir among the `paths` (i.e. `common_dir = ""`), then it is possible for
|
|
1091
|
+
// some `subpath`s to contain a leading dotslash ("./"), in which case we must trim it before concatenation
|
|
1092
|
+
subpath = (string_starts_with(subpath, dotslash) ? subpath.slice(2) : subpath);
|
|
1093
|
+
return new_common_dir + subpath;
|
|
1094
|
+
});
|
|
1095
|
+
};
|
|
1096
|
+
/** get the file name from a given normalized posix path.
|
|
1097
|
+
* if the provided path ends with a trailing slash ("/"), then an empty string will be returned, emphasizing the lack of a file name.
|
|
1098
|
+
*
|
|
1099
|
+
* @example
|
|
1100
|
+
* ```ts ignore
|
|
1101
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
1102
|
+
*
|
|
1103
|
+
* // aliasing our functions for brevity
|
|
1104
|
+
* const eq = assertEquals, fn = parseNormalizedPosixFilename
|
|
1105
|
+
*
|
|
1106
|
+
* eq(fn("/home/user/docs"), "docs")
|
|
1107
|
+
* eq(fn("/home/user/docs.md"), "docs.md")
|
|
1108
|
+
* eq(fn("/home/user/.bashrc"), ".bashrc")
|
|
1109
|
+
* eq(fn("var/log.txt"), "log.txt")
|
|
1110
|
+
* eq(fn("log"), "log")
|
|
1111
|
+
* eq(fn("C:/a/b/c/etc"), "etc")
|
|
1112
|
+
*
|
|
1113
|
+
* eq(fn("/home/user/.config/"), "")
|
|
1114
|
+
* eq(fn("var/log/"), "")
|
|
1115
|
+
* eq(fn("C:/a/b/c/etc/"), "")
|
|
1116
|
+
* eq(fn(""), "")
|
|
1117
|
+
* eq(fn("/"), "")
|
|
1118
|
+
* eq(fn("///"), "")
|
|
1119
|
+
* ```
|
|
1120
|
+
*/
|
|
1121
|
+
const parseNormalizedPosixFilename = (file_path) => {
|
|
1122
|
+
return trimStartSlashes(filename_regex.exec(file_path)?.[0] ?? "");
|
|
1123
|
+
};
|
|
1124
|
+
/** get the base name and extension name of a file, from its filename (no directories).
|
|
1125
|
+
*
|
|
1126
|
+
* @example
|
|
1127
|
+
* ```ts ignore
|
|
1128
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
1129
|
+
*
|
|
1130
|
+
* // aliasing our functions for brevity
|
|
1131
|
+
* const eq = assertEquals, fn = parseBasenameAndExtname_FromFilename
|
|
1132
|
+
*
|
|
1133
|
+
* eq(fn("docs"), ["docs", ""])
|
|
1134
|
+
* eq(fn("docs."), ["docs.", ""])
|
|
1135
|
+
* eq(fn("docs.md"), ["docs", ".md"])
|
|
1136
|
+
* eq(fn(".bashrc"), [".bashrc", ""])
|
|
1137
|
+
* eq(fn("my file.tar.gz"), ["my file.tar", ".gz"])
|
|
1138
|
+
* eq(fn(""), ["", ""])
|
|
1139
|
+
* eq(fn("."), [".", ""])
|
|
1140
|
+
* eq(fn(".."), ["..", ""])
|
|
1141
|
+
* eq(fn("...hello"), ["..", ".hello"])
|
|
1142
|
+
* ```
|
|
1143
|
+
*/
|
|
1144
|
+
const parseBasenameAndExtname_FromFilename = (filename) => {
|
|
1145
|
+
const { basename = "", ext = "" } = basename_and_extname_regex.exec(filename)?.groups ?? {};
|
|
1146
|
+
return [basename, ext];
|
|
1147
|
+
};
|
|
1148
|
+
/** parses the provided file path and breaks it down into useful bit described by the interface {@link FilepathInfo}.
|
|
1149
|
+
* note that a file path must never end in a trailing slash ("/"), and conversely,
|
|
1150
|
+
* a folder path must always in a trailing slash ("/"), otherwise it will be parsed as a file.
|
|
1151
|
+
*
|
|
1152
|
+
* @example
|
|
1153
|
+
* ```ts
|
|
1154
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
1155
|
+
*
|
|
1156
|
+
* // aliasing our functions for brevity
|
|
1157
|
+
* const eq = assertEquals, fn = parseFilepathInfo
|
|
1158
|
+
*
|
|
1159
|
+
* eq(fn("/home\\user/docs"), {
|
|
1160
|
+
* path: "/home/user/docs",
|
|
1161
|
+
* dirpath: "/home/user/",
|
|
1162
|
+
* dirname: "user",
|
|
1163
|
+
* filename: "docs",
|
|
1164
|
+
* basename: "docs",
|
|
1165
|
+
* extname: "",
|
|
1166
|
+
* })
|
|
1167
|
+
*
|
|
1168
|
+
* eq(fn("home\\user/docs/"), {
|
|
1169
|
+
* path: "home/user/docs/",
|
|
1170
|
+
* dirpath: "home/user/docs/",
|
|
1171
|
+
* dirname: "docs",
|
|
1172
|
+
* filename: "",
|
|
1173
|
+
* basename: "",
|
|
1174
|
+
* extname: "",
|
|
1175
|
+
* })
|
|
1176
|
+
*
|
|
1177
|
+
* eq(fn("/home/xyz/.././././user/.bashrc."), {
|
|
1178
|
+
* path: "/home/user/.bashrc.",
|
|
1179
|
+
* dirpath: "/home/user/",
|
|
1180
|
+
* dirname: "user",
|
|
1181
|
+
* filename: ".bashrc.",
|
|
1182
|
+
* basename: ".bashrc.",
|
|
1183
|
+
* extname: "",
|
|
1184
|
+
* })
|
|
1185
|
+
*
|
|
1186
|
+
* eq(fn("C:\\home\\user/.file.tar.gz"), {
|
|
1187
|
+
* path: "C:/home/user/.file.tar.gz",
|
|
1188
|
+
* dirpath: "C:/home/user/",
|
|
1189
|
+
* dirname: "user",
|
|
1190
|
+
* filename: ".file.tar.gz",
|
|
1191
|
+
* basename: ".file.tar",
|
|
1192
|
+
* extname: ".gz", // only the last bit of the extension makes it to here
|
|
1193
|
+
* })
|
|
1194
|
+
*
|
|
1195
|
+
* eq(fn("/home/user///file.txt"), {
|
|
1196
|
+
* path: "/home/user///file.txt",
|
|
1197
|
+
* dirpath: "/home/user///",
|
|
1198
|
+
* dirname: "", // this is because the there is no name attached between the last two slashes of the `dirpath = "/home/user///"`
|
|
1199
|
+
* filename: "file.txt",
|
|
1200
|
+
* basename: "file",
|
|
1201
|
+
* extname: ".txt",
|
|
1202
|
+
* })
|
|
1203
|
+
*
|
|
1204
|
+
* eq(fn("file://C:/home\\hello world.txt"), {
|
|
1205
|
+
* path: "file://C:/home/hello world.txt", // file-urls are not converted, nor is any kind of url
|
|
1206
|
+
* dirpath: "file://C:/home/",
|
|
1207
|
+
* dirname: "home",
|
|
1208
|
+
* filename: "hello world.txt",
|
|
1209
|
+
* basename: "hello world",
|
|
1210
|
+
* extname: ".txt",
|
|
1211
|
+
* })
|
|
1212
|
+
* ```
|
|
1213
|
+
*/
|
|
1214
|
+
export const parseFilepathInfo = (file_path) => {
|
|
1215
|
+
const path = normalizePath(file_path), filename = parseNormalizedPosixFilename(path), filename_length = filename.length, dirpath = filename_length > 0 ? path.slice(0, -filename_length) : path,
|
|
1216
|
+
// below, I am purposely using `slice` instead of doing `trimEndSlashes(dirpath)`, because it is possible that two or more consecutive slashes "/" were intentionally placed in the directory separator.
|
|
1217
|
+
dirname = parseNormalizedPosixFilename(dirpath.slice(0, -1)), [basename, extname] = parseBasenameAndExtname_FromFilename(filename);
|
|
1218
|
+
return { path, dirpath, dirname, filename, basename, extname, };
|
|
1219
|
+
};
|
|
1220
|
+
/** convert the input file-url to a filesystem local-path.
|
|
1221
|
+
* however, if the input uri is not a file url (for instance `"C:/x/y/z"`, or `"http://hello.com"`),
|
|
1222
|
+
* then `undefined` will be returned.
|
|
1223
|
+
*
|
|
1224
|
+
* if you are looking to convert any _potential_ file-url back to a filesystem local-path,
|
|
1225
|
+
* then the {@link ensureFileUrlIsLocalPath} function would be better suited for your need.
|
|
1226
|
+
*
|
|
1227
|
+
* @example
|
|
1228
|
+
* ```ts
|
|
1229
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
1230
|
+
*
|
|
1231
|
+
* // aliasing our functions for brevity
|
|
1232
|
+
* const
|
|
1233
|
+
* fn = fileUrlToLocalPath,
|
|
1234
|
+
* eq = assertEquals
|
|
1235
|
+
*
|
|
1236
|
+
* eq( fn("file:///C:/Users/me/projects/"), "C:/Users/me/projects/")
|
|
1237
|
+
* eq( fn("file:///C:\\Users\\me/projects/"), "C:/Users/me/projects/")
|
|
1238
|
+
* eq( fn("file:///sys/etc/bin/deno.so"), "/sys/etc/bin/deno.so")
|
|
1239
|
+
* eq( fn("file:///sys\\etc/bin\\deno.so"), "/sys/etc/bin/deno.so")
|
|
1240
|
+
* eq( fn("file://localhost/C:/Users/me/projects/"), "C:/Users/me/projects/")
|
|
1241
|
+
* eq( fn("file://localhost/sys/etc/bin/deno.so"), "/sys/etc/bin/deno.so")
|
|
1242
|
+
* eq(fn(new URL("file:///C:/Users/me/projects/")), "C:/Users/me/projects/")
|
|
1243
|
+
* eq(fn(new URL("file:///sys/etc/bin/deno.so")), "/sys/etc/bin/deno.so")
|
|
1244
|
+
* eq(fn(new URL("file://localhost/C:/Users/me/projects/")), "C:/Users/me/projects/")
|
|
1245
|
+
* eq(fn(new URL("file://localhost/sys/etc/bin/deno.so")), "/sys/etc/bin/deno.so")
|
|
1246
|
+
*
|
|
1247
|
+
* // everything below is not a file-url, and therefore cannot be converted.
|
|
1248
|
+
* eq( fn("http://localhost:8000/hello/world/"), undefined)
|
|
1249
|
+
* eq( fn("C:/Users/me/projects/"), undefined)
|
|
1250
|
+
* eq( fn("/sys/etc/bin/deno.so"), undefined)
|
|
1251
|
+
* eq( fn(""), undefined)
|
|
1252
|
+
* ```
|
|
1253
|
+
*/
|
|
1254
|
+
export const fileUrlToLocalPath = (file_url) => {
|
|
1255
|
+
if (isString(file_url)) {
|
|
1256
|
+
if (getUriScheme(file_url) !== "file") {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
file_url = new URL(file_url);
|
|
1260
|
+
}
|
|
1261
|
+
if (!string_starts_with(file_url.protocol, "file:")) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
// the `file_url.pathname` always starts with a leading slash, which is invalid for windows.
|
|
1265
|
+
// thus we replace any leading slashes in any windows-looking path that we encounter, without actually consulting what os is being ran.
|
|
1266
|
+
const local_path_with_leading_slash = pathToPosixPath(dom_decodeURI(file_url.pathname)), corrected_local_path = local_path_with_leading_slash.replace(windows_leading_slash_correction_regex, "$1:/");
|
|
1267
|
+
return corrected_local_path;
|
|
1268
|
+
};
|
|
1269
|
+
/** a fault tolerant variant of {@link fileUrlToLocalPath} that assures you that any file-url path will get converted into a filesystem local-path.
|
|
1270
|
+
* otherwise, when a non-file-url is provided, its string representation (href) will be returned if it was a `URL`,
|
|
1271
|
+
* else the original string will be returned back.
|
|
1272
|
+
*
|
|
1273
|
+
* @example
|
|
1274
|
+
* ```ts
|
|
1275
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
1276
|
+
*
|
|
1277
|
+
* // aliasing our functions for brevity
|
|
1278
|
+
* const
|
|
1279
|
+
* fn = ensureFileUrlIsLocalPath,
|
|
1280
|
+
* eq = assertEquals
|
|
1281
|
+
*
|
|
1282
|
+
* eq( fn("C:/Users/me/projects/"), "C:/Users/me/projects/")
|
|
1283
|
+
* eq( fn("C:\\Users\\me/projects/"), "C:/Users/me/projects/")
|
|
1284
|
+
* eq( fn("/C:/Users\\me/projects/"), "/C:/Users/me/projects/") // note the erroneous leading slash
|
|
1285
|
+
* eq( fn("/sys\\etc/bin\\deno.so"), "/sys/etc/bin/deno.so")
|
|
1286
|
+
* eq( fn("file:///C:/Users/me/projects/"), "C:/Users/me/projects/")
|
|
1287
|
+
* eq( fn("file://////C:\\Users\\me/projects/"), "C:/Users/me/projects/")
|
|
1288
|
+
* eq( fn("file:///sys\\etc/bin\\deno.so"), "/sys/etc/bin/deno.so")
|
|
1289
|
+
* eq( fn("file://localhost/C:/Users/me/projects/"), "C:/Users/me/projects/")
|
|
1290
|
+
* eq(fn(new URL("file://localhost/sys/etc/bin/deno.so")), "/sys/etc/bin/deno.so")
|
|
1291
|
+
* eq( fn("http://localhost:8000/hello/world/"), "http://localhost:8000/hello/world/")
|
|
1292
|
+
* eq( fn("npm:react-jsx"), "npm:react-jsx")
|
|
1293
|
+
* eq( fn("jsr:@std/assert"), "jsr:@std/assert")
|
|
1294
|
+
* eq( fn("./src/mod.ts"), "./src/mod.ts")
|
|
1295
|
+
* eq( fn(""), "")
|
|
1296
|
+
* ```
|
|
1297
|
+
*/
|
|
1298
|
+
export const ensureFileUrlIsLocalPath = (path) => {
|
|
1299
|
+
const path_is_string = isString(path), file_uri_to_local_path_conversion = fileUrlToLocalPath(path);
|
|
1300
|
+
return file_uri_to_local_path_conversion ?? (path_is_string
|
|
1301
|
+
? pathToPosixPath(path)
|
|
1302
|
+
: path.href);
|
|
1303
|
+
};
|
|
1304
|
+
/** find the path `to_path`, relative to `from_path`.
|
|
1305
|
+
*
|
|
1306
|
+
* TODO: the claim below is wrong, because `joinSlash` cannot do "./" traversal correctly. for instance `joinSlash("a/b/c.txt", "./") === "a/b/c.txt/"`, but you'd expect it to be `"a/b/"` if we had correctly resolved the path.
|
|
1307
|
+
* which is why the output from this function will not work with `joinSlash`, but it will work with {@link resolveAsUrl} or `URL.parse` (when you provide `from_path` as the base path, and `from_path` is NOT a relative path, otherwise the `URL` constructor will fail).
|
|
1308
|
+
*
|
|
1309
|
+
* ~~if we call the result `rel_path`, then {@link joinSlash | joining} the `from_path` with `rel_path` and normalizing it should give you back `to_path`.~~
|
|
1310
|
+
*
|
|
1311
|
+
* note that both `from_path` and `to_path` must have a common root folder in their path strings.
|
|
1312
|
+
* for instance, if both paths begin with a relative segment `"./"`, then it will be assumed that both paths are referring to the same common root ancestral directory.
|
|
1313
|
+
* however, if for instance, `from_path` begins with a `"C:/"` segment, while `to_path` begins with either `"./"` or `"D:/"` or `http://` segment, then this function will fail,
|
|
1314
|
+
* as it will not be possible for it to navigate/transcend from one point of reference to a completely different point of reference.
|
|
1315
|
+
*
|
|
1316
|
+
* so, to be safe, wherever you are certain that both paths are of a certain common type:
|
|
1317
|
+
* before passing them here, you should either apply the {@link ensureStartDotSlash} function for relative paths, or apply the {@link ensureStartSlash} for absolute local paths,
|
|
1318
|
+
* or write a custom "ensure" function for your situation. (for example, you could write an "ensureHttp" function that ensures that your path begins with `"http"`).
|
|
1319
|
+
*
|
|
1320
|
+
* @throws `Error` an error will be thrown if there isn't any common ancestral directory between the two provided paths.
|
|
1321
|
+
*
|
|
1322
|
+
* @example
|
|
1323
|
+
* ```ts
|
|
1324
|
+
* import { assertEquals, assertThrows } from "jsr:@std/assert"
|
|
1325
|
+
*
|
|
1326
|
+
* // aliasing our functions for brevity
|
|
1327
|
+
* const eq = assertEquals, err = assertThrows, fn = relativePath
|
|
1328
|
+
*
|
|
1329
|
+
* eq(fn(
|
|
1330
|
+
* "././hello/world/a/b/c/d/g/../e.txt",
|
|
1331
|
+
* "././hello/world/a/b/x/y/w/../z/",
|
|
1332
|
+
* ), "../../x/y/z/")
|
|
1333
|
+
*
|
|
1334
|
+
* eq(fn(
|
|
1335
|
+
* "././hello/world/a/b/c/d/g/../e.txt",
|
|
1336
|
+
* "././hello/world/a/b/x/y/w/../z/e.md",
|
|
1337
|
+
* ), "../../x/y/z/e.md")
|
|
1338
|
+
*
|
|
1339
|
+
* eq(fn(
|
|
1340
|
+
* ".\\./hello\\world\\a/b\\c/d/g/../",
|
|
1341
|
+
* "././hello/world/a/b/x/y/w/../z/e.md",
|
|
1342
|
+
* ), "../../x/y/z/e.md")
|
|
1343
|
+
*
|
|
1344
|
+
* eq(fn(
|
|
1345
|
+
* "././hello/world/a/b/c/d/",
|
|
1346
|
+
* "././hello/world/a/b/x/y/w/../z/e.md",
|
|
1347
|
+
* ), "../../x/y/z/e.md")
|
|
1348
|
+
*
|
|
1349
|
+
* eq(fn(
|
|
1350
|
+
* "././hello/world/a/b/c/d/g/../",
|
|
1351
|
+
* "././hello/world/a/b/x/y/w/../z/e.md",
|
|
1352
|
+
* ), "../../x/y/z/e.md")
|
|
1353
|
+
*
|
|
1354
|
+
* eq(fn(
|
|
1355
|
+
* "././hello/world/a/b/c/d/",
|
|
1356
|
+
* "././hello/world/a/b/x/y/w/../z/",
|
|
1357
|
+
* ), "../../x/y/z/")
|
|
1358
|
+
*
|
|
1359
|
+
* eq(fn(
|
|
1360
|
+
* "./././e.txt",
|
|
1361
|
+
* "./e.md",
|
|
1362
|
+
* ), "./e.md")
|
|
1363
|
+
*
|
|
1364
|
+
* eq(fn(
|
|
1365
|
+
* "/e.txt",
|
|
1366
|
+
* "/e.md",
|
|
1367
|
+
* ), "./e.md")
|
|
1368
|
+
*
|
|
1369
|
+
* eq(fn(
|
|
1370
|
+
* "C:/e.txt",
|
|
1371
|
+
* "C:/e.md",
|
|
1372
|
+
* ), "./e.md")
|
|
1373
|
+
*
|
|
1374
|
+
* eq(fn(
|
|
1375
|
+
* "././hello/world/a/b/c/d/g/../e.txt",
|
|
1376
|
+
* "././hello/world/a/k/../b/q/../c/d/e.md",
|
|
1377
|
+
* ), "./e.md")
|
|
1378
|
+
*
|
|
1379
|
+
* eq(fn(
|
|
1380
|
+
* "./",
|
|
1381
|
+
* "./",
|
|
1382
|
+
* ), "./")
|
|
1383
|
+
*
|
|
1384
|
+
* eq(fn(
|
|
1385
|
+
* "/",
|
|
1386
|
+
* "/",
|
|
1387
|
+
* ), "./")
|
|
1388
|
+
*
|
|
1389
|
+
* // there is no common ancestral root between the two paths (since one is absolute, while the other is relative)
|
|
1390
|
+
* err(() => fn(
|
|
1391
|
+
* "/e.txt",
|
|
1392
|
+
* "./e.md",
|
|
1393
|
+
* ))
|
|
1394
|
+
*
|
|
1395
|
+
* // there is no common ancestral root between the two paths
|
|
1396
|
+
* err(() => fn(
|
|
1397
|
+
* "C:/e.txt",
|
|
1398
|
+
* "D:/e.md",
|
|
1399
|
+
* ))
|
|
1400
|
+
*
|
|
1401
|
+
* // there is no common ancestral root between the two paths
|
|
1402
|
+
* err(() => fn(
|
|
1403
|
+
* "http://e.txt",
|
|
1404
|
+
* "./e.md",
|
|
1405
|
+
* ))
|
|
1406
|
+
*
|
|
1407
|
+
* // there is no common ancestral root between the two paths
|
|
1408
|
+
* err(() => fn(
|
|
1409
|
+
* "file:///C:/e.txt",
|
|
1410
|
+
* "C:/e.md",
|
|
1411
|
+
* ))
|
|
1412
|
+
* ```
|
|
1413
|
+
*/
|
|
1414
|
+
export const relativePath = (from_path, to_path) => {
|
|
1415
|
+
const [[common_dir, from_subpath], [, to_subpath],] = commonPathTransform([from_path, to_path], (common_dir_and_subpath) => common_dir_and_subpath);
|
|
1416
|
+
if (common_dir === "") {
|
|
1417
|
+
throw new Error(DEBUG.ERROR ? (`there is no common directory between the two provided paths:\n\t"${from_path}" and\n\t"to_path"`) : "");
|
|
1418
|
+
}
|
|
1419
|
+
const upwards_traversal = Array(from_subpath.split(sep).length).fill("..");
|
|
1420
|
+
upwards_traversal[0] = "."; // we do this because the relative path should always begin with a dotslash ("./")
|
|
1421
|
+
return normalizePosixPath(upwards_traversal.join(sep) + sep + to_subpath);
|
|
1422
|
+
};
|
|
1423
|
+
/** the reduce function below is used by {@link joinPosixPaths}, and it is basically the driving force behind it.
|
|
1424
|
+
*
|
|
1425
|
+
* we could have used a `string` based reduce function (i.e. initial value could have been the string `"/"`, instead of the array `["/"]`),
|
|
1426
|
+
* but we use an `Array` based reduce function, because strings are inherently immutable, so a new string is made during each modification.
|
|
1427
|
+
* and that would use up lots of copy operations if you were joining lots of segments.
|
|
1428
|
+
* it is better to process them as an array and then join it in one go at the end.
|
|
1429
|
+
*/
|
|
1430
|
+
const joinPosixPaths_reduce_fn = (concatenatible_full_path, segment) => {
|
|
1431
|
+
const prev_segment = concatenatible_full_path.pop(), prev_segment_is_dir = string_ends_with(prev_segment, sep), prev_segment_as_dir = prev_segment_is_dir ? prev_segment : (prev_segment + sep); // rewriting the previous segment as a dir
|
|
1432
|
+
if (!prev_segment_is_dir) {
|
|
1433
|
+
const segment_is_rel_to_dir = string_starts_with(segment, dotslash), segment_is_rel_to_parent_dir = string_starts_with(segment, dotdotslash);
|
|
1434
|
+
// we now modify the current segment's initial directory navigation commands to give an equivalent navigation supposing that `prev_segment` was a directory instead of a file.
|
|
1435
|
+
// for that, we convert the initial `"./"` to `"../"`, or convert the initial `"../"` to `"../../"`, or
|
|
1436
|
+
// if there is no directory navigation command at the beginning, then no modification to the current segment is needed.
|
|
1437
|
+
if (segment_is_rel_to_dir) {
|
|
1438
|
+
segment = "." + segment;
|
|
1439
|
+
} // convert `"./a/b/c"` to `"../a/b/c"`
|
|
1440
|
+
else if (segment_is_rel_to_parent_dir) {
|
|
1441
|
+
segment = dotdotslash + segment;
|
|
1442
|
+
} // convert `"../a/b/c"` to `"../../a/b/c"`
|
|
1443
|
+
}
|
|
1444
|
+
concatenatible_full_path.push(prev_segment_as_dir, segment);
|
|
1445
|
+
return concatenatible_full_path;
|
|
1446
|
+
};
|
|
1447
|
+
/** joins multiple posix path segments into a single normalized path,
|
|
1448
|
+
* correctly handling files and directories differently when the `"./"` and `"../"` navigation commands are encountered.
|
|
1449
|
+
*
|
|
1450
|
+
* > [!note]
|
|
1451
|
+
* > the `joinPosixPaths` function differs from `joinSlash` in that it treats segments without a trailing slash as files by default.
|
|
1452
|
+
* > furthermore, `joinPosixPaths` is much more quicker to compute, as opposed to `joinSlash`, which uses some complex regex on each segment.
|
|
1453
|
+
* > the only reason you might realistically want to use `joinSlash` is when you desire an non-normalized output.
|
|
1454
|
+
*
|
|
1455
|
+
* @example
|
|
1456
|
+
* ```ts
|
|
1457
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
1458
|
+
*
|
|
1459
|
+
* // aliasing our functions for brevity
|
|
1460
|
+
* const eq = assertEquals, fn = joinPosixPaths
|
|
1461
|
+
*
|
|
1462
|
+
* eq(fn("a", "b", "c.zip"), "a/b/c.zip")
|
|
1463
|
+
* eq(fn("a", "b", "c/"), "a/b/c/")
|
|
1464
|
+
* eq(fn("a/", "b/", "c/"), "a/b/c/")
|
|
1465
|
+
* eq(fn("a", "b", "c.zip", "./"), "a/b/")
|
|
1466
|
+
* eq(fn("a", "b", "c/", "./"), "a/b/c/")
|
|
1467
|
+
* eq(fn("a", "b", "c", "../"), "a/")
|
|
1468
|
+
* eq(fn("a", "b", "c/d.txt", "./e.txt"), "a/b/c/e.txt")
|
|
1469
|
+
* eq(fn("a", "b", "c/d.txt", "../e.txt"), "a/b/e.txt")
|
|
1470
|
+
* eq(fn("a/b", "c/d.txt", "..", "e.txt"), "a/b/e.txt") // notice that you can use "." instead of "./"
|
|
1471
|
+
* eq(fn("a/", "b/", "c/", "../", "./","d.txt"), "a/b/d.txt")
|
|
1472
|
+
* eq(fn("a/b", "c/", "..", ".","d.txt"), "a/b/d.txt") // notice that you can use ".." instead of "../"
|
|
1473
|
+
* eq(fn("a/b", "c/", ".d.txt"), "a/b/c/.d.txt")
|
|
1474
|
+
* eq(fn("a/b", "c/", "..d.txt"), "a/b/c/..d.txt")
|
|
1475
|
+
* eq(fn("a/b", "c/", ".d"), "a/b/c/.d")
|
|
1476
|
+
* eq(fn("a/b", "c/", ".d."), "a/b/c/.d.")
|
|
1477
|
+
* eq(fn("a/b", "c/", "..d"), "a/b/c/..d")
|
|
1478
|
+
* eq(fn("a/b", "c/", "..d."), "a/b/c/..d.")
|
|
1479
|
+
* eq(fn("a/b", "c/", "..d.."), "a/b/c/..d..")
|
|
1480
|
+
* eq(fn("a/b", "c/", "..d.", "e.txt"), "a/b/c/..d./e.txt")
|
|
1481
|
+
* eq(fn("a/b", "c/", "..d..", "e.txt"), "a/b/c/..d../e.txt")
|
|
1482
|
+
* eq(fn("./a", "./b", "./c"), "./c")
|
|
1483
|
+
* eq(fn("./a", "/b", "/c"), "./a//b//c")
|
|
1484
|
+
* eq(fn("./a/", "/b/", "/c"), "./a//b//c")
|
|
1485
|
+
* eq(fn("/a", "b.zip", "./c.zip"), "/a/c.zip")
|
|
1486
|
+
* eq(fn("/a", "b.zip", "./c.zip/", "../d.txt"), "/a/d.txt")
|
|
1487
|
+
* eq(fn("/a", "b.zip/", "./c.txt"), "/a/b.zip/c.txt")
|
|
1488
|
+
* eq(fn("/a", "b.zip/", "./c.txt/", "../d.txt"), "/a/b.zip/d.txt")
|
|
1489
|
+
* eq(fn("file:", "//", "a/b/c", "./d"), "file:///a/b/d")
|
|
1490
|
+
* eq(fn("file:///", "a/b/c", "./d"), "file:///a/b/d")
|
|
1491
|
+
* ```
|
|
1492
|
+
*/
|
|
1493
|
+
export const joinPosixPaths = (...segments) => {
|
|
1494
|
+
// first we ensure that all segments that are purely ill-formed directory commands ("." and ".."), become well-formed ("./" and "../")
|
|
1495
|
+
segments = segments.map((segment) => {
|
|
1496
|
+
return segment === "." ? dotslash
|
|
1497
|
+
: segment === ".." ? dotdotslash
|
|
1498
|
+
: segment;
|
|
1499
|
+
});
|
|
1500
|
+
// for better optimization, I've moved the reduction closure function to the outside scope (`joinPosixPaths_reduce_fn`).
|
|
1501
|
+
const concatenatible_segments = segments.reduce(joinPosixPaths_reduce_fn, [sep]);
|
|
1502
|
+
concatenatible_segments.shift(); // we must remove the initial `"/"` that was used as the initial value of the reduce function
|
|
1503
|
+
return normalizePosixPath(concatenatible_segments.join(""));
|
|
1504
|
+
};
|
|
1505
|
+
/** joins multiple path segments into a single normalized posix path,
|
|
1506
|
+
* correctly handling files and directories differently when the `"./"` and `"../"` navigation commands are encountered.
|
|
1507
|
+
*
|
|
1508
|
+
* for lots of other (posix-only) test cases, see the doc comments of {@link joinPosixPaths}.
|
|
1509
|
+
*
|
|
1510
|
+
* @example
|
|
1511
|
+
* ```ts
|
|
1512
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
1513
|
+
*
|
|
1514
|
+
* // aliasing our functions for brevity
|
|
1515
|
+
* const eq = assertEquals, fn = joinPaths
|
|
1516
|
+
*
|
|
1517
|
+
* eq(fn("C:\\", "a", "b", "c.zip"), "C:/a/b/c.zip")
|
|
1518
|
+
* eq(fn("a", "b", "c.zip"), "a/b/c.zip")
|
|
1519
|
+
* eq(fn("/a\\b\\", "c/"), "/a/b/c/")
|
|
1520
|
+
* eq(fn("a", "b", "c.zip", "./"), "a/b/")
|
|
1521
|
+
* eq(fn("a", "b", "c.zip", "../"), "a/")
|
|
1522
|
+
* eq(fn("a", "b", "c.zip", "./d.txt"), "a/b/d.txt")
|
|
1523
|
+
* eq(fn("a", "b", "c.zip", "../d.txt"), "a/d.txt")
|
|
1524
|
+
* eq(fn("a/b/c/", "..", "./", "d.txt"), "a/b/d.txt")
|
|
1525
|
+
* eq(fn("a/b/c/", "../", ".", "d.txt"), "a/b/d.txt")
|
|
1526
|
+
* eq(fn("a/b/c", "../", "./", "d.txt"), "a/d.txt")
|
|
1527
|
+
* eq(fn("file:", "\\\\", "a/b/c", "./d"), "file:///a/b/d")
|
|
1528
|
+
* eq(fn("file://\\", "a/b/c", "./d/"), "file:///a/b/d/")
|
|
1529
|
+
* ```
|
|
1530
|
+
*/
|
|
1531
|
+
export const joinPaths = (...segments) => {
|
|
1532
|
+
return joinPosixPaths(...segments.map(pathToPosixPath));
|
|
1533
|
+
};
|
|
1534
|
+
/** this is a factory function for creating customizable posix path-resolving functions.
|
|
1535
|
+
* a path-resolving function is one that takes in a list of path-segments, and then computes the absolute normalized path of all the segments combined.
|
|
1536
|
+
*
|
|
1537
|
+
* since it is not possible for this submodule to know the context under which you are computing/resolving paths,
|
|
1538
|
+
* it becomes impossible to give a meaning to a list of path segmensts that begin with a relative path.
|
|
1539
|
+
* which is why you would need to feed this factory function with the (static or dynamic) location of your current working path (directory),
|
|
1540
|
+
* and it will spit out a path-resolver function that is tailored to your specific working-directory's path.
|
|
1541
|
+
*
|
|
1542
|
+
* > [!note]
|
|
1543
|
+
* > if you want to preserve the starting relative segments, (i.e. you don't want an absolute path),
|
|
1544
|
+
* > then you're looking for the {@link joinPosixPaths} (or {@link joinPaths}) function, not this one.
|
|
1545
|
+
*
|
|
1546
|
+
* an important detail to note is that whenever an absolute path segment is encountered, all segments prior to it are discarded
|
|
1547
|
+
* (i.e. not joined with a "/" separator, like the way {@link joinPaths} does).
|
|
1548
|
+
* this behavior is enforced to remain consistent with popular implementations of path-resolvers, like:
|
|
1549
|
+
* - python's pathlib [`Path.resolve`](https://docs.python.org/3/library/pathlib):
|
|
1550
|
+
* > _If a segment is an absolute path, all previous segments are ignored_
|
|
1551
|
+
* - deno-std's path [`resolve`](https://jsr.io/@std/path/doc/~/resolve), from [`jsr:@std/path`](https://jsr.io/@std/path)
|
|
1552
|
+
*
|
|
1553
|
+
* > [!caution]
|
|
1554
|
+
* > this path resolver function works best when only absolute and relative path segments are provided.
|
|
1555
|
+
* > when root (but not necessarily absolute) path segments are encountered, such as `/sys/bin`,
|
|
1556
|
+
* > our function will typically assume that it is referring to an absolute local-filesystem path `/sys/bin`.
|
|
1557
|
+
* >
|
|
1558
|
+
* > but as you may be aware, the "correct" interpretation of the root path depends on the context of the preceding absolute path segment.
|
|
1559
|
+
* > for example:
|
|
1560
|
+
* > - if the preceding absolute path segment was `C:/a/b/c/d.txt`, and the current path segment is `/sys/bin`,
|
|
1561
|
+
* > then our result will be `/sys/bin`, but the result in most implementations (including deno-std's path module) would be `C:/sys/bin`.
|
|
1562
|
+
* > - similarly, if the preceding absolute path segment was `http://test.com/a/b/c/d.txt`, and the current path segment is `/sys/bin`,
|
|
1563
|
+
* > then our result will be `/sys/bin`, but the "correct" url path resolution (via `new URL(...)`) should have been `http://test.com/sys.bin`.
|
|
1564
|
+
* >
|
|
1565
|
+
* > this is why, if you're dealing with ambiguous root paths that are not necessarily tied down to your posix-filesystem's root,
|
|
1566
|
+
* > you should use the {@link resolveAsUrl} function instead, as it is aware of most common types of base contexts/domains for path evaluation.
|
|
1567
|
+
* > but on the other hand, you will be unable to use your custom {@link absolute_segment_test_fn}, nor be able to resolve multiple segments all at oncce.
|
|
1568
|
+
* >
|
|
1569
|
+
* > TODO: contemplate if it would be a good idea to add a configuration interface to this factory function,
|
|
1570
|
+
* > where one will be able to set their custom join rule when a root path segment is discovered, in addition to containing the {@link absolute_segment_test_fn}.
|
|
1571
|
+
* > the signature of the root-join function would look like: `(abs_path_evaluated_up_till_now: string, current_root_path_segment: string) => string`.
|
|
1572
|
+
*
|
|
1573
|
+
* @example
|
|
1574
|
+
* ```ts
|
|
1575
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
1576
|
+
*
|
|
1577
|
+
* const
|
|
1578
|
+
* cwd = "/x/y/z",
|
|
1579
|
+
* getCwd = () => (cwd),
|
|
1580
|
+
* // we also define a custom absolute segment path tester function that will identify "file://" and "http://" segments as absolute path,
|
|
1581
|
+
* // in addition to the standard filesystem local path absoluteness tester `isAbsolutePath`.
|
|
1582
|
+
* custom_absolute_path_segment_tester = (segment: string) => {
|
|
1583
|
+
* if (isAbsolutePath(segment)) { return true }
|
|
1584
|
+
* if (segment.startsWith("file://")) { return true }
|
|
1585
|
+
* if (segment.startsWith("http://")) { return true }
|
|
1586
|
+
* return false
|
|
1587
|
+
* },
|
|
1588
|
+
* resolvePosixPath = resolvePosixPathFactory(getCwd, custom_absolute_path_segment_tester)
|
|
1589
|
+
*
|
|
1590
|
+
* // aliasing our functions for brevity
|
|
1591
|
+
* const eq = assertEquals, fn = resolvePosixPath
|
|
1592
|
+
*
|
|
1593
|
+
* // relative path resolution
|
|
1594
|
+
* eq(fn("a", "b", "c.zip"), "/x/y/z/a/b/c.zip")
|
|
1595
|
+
* eq(fn("./a", "b", "c/"), "/x/y/z/a/b/c/")
|
|
1596
|
+
* eq(fn("a/", "b/", "c/"), "/x/y/z/a/b/c/")
|
|
1597
|
+
* eq(fn("../a", "../b", "c/"), "/x/b/c/")
|
|
1598
|
+
* eq(fn("../a/", "../b", "c/"), "/x/y/b/c/")
|
|
1599
|
+
* eq(fn("a", "b", "c.zip", "./"), "/x/y/z/a/b/")
|
|
1600
|
+
* eq(fn("a", "b", "c/", "./"), "/x/y/z/a/b/c/")
|
|
1601
|
+
* eq(fn("a", "b", "c", "../"), "/x/y/z/a/")
|
|
1602
|
+
* eq(fn("a", "b", "c/d.txt", "./e.txt"), "/x/y/z/a/b/c/e.txt")
|
|
1603
|
+
* eq(fn("a", "b", "c/d.txt", "../e.txt"), "/x/y/z/a/b/e.txt")
|
|
1604
|
+
* eq(fn("a/b", "c/d.txt", "..", "e.txt"), "/x/y/z/a/b/e.txt") // notice that you can use "." instead of "./"
|
|
1605
|
+
* eq(fn("a/", "b/", "c/", "../", "./","d.txt"), "/x/y/z/a/b/d.txt")
|
|
1606
|
+
* eq(fn("a/b", "c/", "..", ".","d.txt"), "/x/y/z/a/b/d.txt") // notice that you can use ".." instead of "../"
|
|
1607
|
+
* eq(fn("a/b", "c/", ".d.txt"), "/x/y/z/a/b/c/.d.txt")
|
|
1608
|
+
* eq(fn("a/b", "c/", "..d.txt"), "/x/y/z/a/b/c/..d.txt")
|
|
1609
|
+
* eq(fn("a/b", "c/", ".d"), "/x/y/z/a/b/c/.d")
|
|
1610
|
+
* eq(fn("a/b", "c/", ".d."), "/x/y/z/a/b/c/.d.")
|
|
1611
|
+
* eq(fn("a/b", "c/", "..d"), "/x/y/z/a/b/c/..d")
|
|
1612
|
+
* eq(fn("a/b", "c/", "..d."), "/x/y/z/a/b/c/..d.")
|
|
1613
|
+
* eq(fn("a/b", "c/", "..d.."), "/x/y/z/a/b/c/..d..")
|
|
1614
|
+
* eq(fn("a/b", "c/", "..d.", "e.txt"), "/x/y/z/a/b/c/..d./e.txt")
|
|
1615
|
+
* eq(fn("a/b", "c/", "..d..", "e.txt"), "/x/y/z/a/b/c/..d../e.txt")
|
|
1616
|
+
* eq(fn("./a", "./b", "./c"), "/x/y/z/c")
|
|
1617
|
+
*
|
|
1618
|
+
* // pre-existing absolute path resolution
|
|
1619
|
+
* eq(fn("./a", "/b", "/c"), "/c") // both "/b" and "/c" are absolute paths, and so they purge all segments behind them.
|
|
1620
|
+
* eq(fn("./a/", "/b/", "./c"), "/b/c") // "/b/" is an absolute path, hence it negates all segments prior to it.
|
|
1621
|
+
* eq(fn("file:///", "a/b/c", "./d"), "file:///a/b/d") // the first "file:///" segment is an absolute path according to our `custom_absolute_path_segment_tester`
|
|
1622
|
+
* eq(fn("file:/", "a/b/c", "./d"), "/x/y/z/file:/a/b/d") // "file:/" is not considered as an absolute path by `custom_absolute_path_segment_tester`, thus it the cwd will be prepended to the final path
|
|
1623
|
+
* eq(fn("file:", "a/b/c", "./d"), "/x/y/z/file:/a/b/d") // same as before, but we're putting emphasis on the mandatory "/" separator that gets added after the "file:" segment
|
|
1624
|
+
* eq(fn("a/b/c", "http://d/e/f.txt"), "http://d/e/f.txt") // the "http://" segment is identified as an absolute path by our `custom_absolute_path_segment_tester`
|
|
1625
|
+
* ```
|
|
1626
|
+
*/
|
|
1627
|
+
export const resolvePosixPathFactory = (absolute_current_dir, absolute_segment_test_fn = isAbsolutePath) => {
|
|
1628
|
+
const getCwdPath = isString(absolute_current_dir)
|
|
1629
|
+
? (() => absolute_current_dir)
|
|
1630
|
+
: absolute_current_dir;
|
|
1631
|
+
return (...segments) => {
|
|
1632
|
+
// first, we find the index of the last segment that is absolutely defined.
|
|
1633
|
+
// if there isn't one, then we use `getCwdPath` as our base path
|
|
1634
|
+
const last_abs_segment_idx = segments.findLastIndex(absolute_segment_test_fn);
|
|
1635
|
+
if (last_abs_segment_idx >= 0) {
|
|
1636
|
+
segments = segments.slice(last_abs_segment_idx);
|
|
1637
|
+
}
|
|
1638
|
+
else {
|
|
1639
|
+
segments.unshift(ensureEndSlash(getCwdPath()));
|
|
1640
|
+
}
|
|
1641
|
+
return joinPosixPaths(...segments);
|
|
1642
|
+
};
|
|
1643
|
+
};
|
|
1644
|
+
/** this is a factory function for creating customizable path-resolving functions.
|
|
1645
|
+
*
|
|
1646
|
+
* {@inheritDoc resolvePosixPathFactory}
|
|
1647
|
+
*
|
|
1648
|
+
* @example
|
|
1649
|
+
* ```ts
|
|
1650
|
+
* import { assertEquals } from "jsr:@std/assert"
|
|
1651
|
+
*
|
|
1652
|
+
* const
|
|
1653
|
+
* cwd = "x:\\y\\z",
|
|
1654
|
+
* getCwd = () => (cwd),
|
|
1655
|
+
* // we also define a custom absolute segment path tester function that will identify "file://" and "http://" segments as absolute path,
|
|
1656
|
+
* // in addition to the standard filesystem local path absoluteness tester `isAbsolutePath`.
|
|
1657
|
+
* custom_absolute_path_segment_tester = (segment: string) => {
|
|
1658
|
+
* if (isAbsolutePath(segment)) { return true }
|
|
1659
|
+
* if (segment.startsWith("file://")) { return true }
|
|
1660
|
+
* if (segment.startsWith("http://")) { return true }
|
|
1661
|
+
* return false
|
|
1662
|
+
* },
|
|
1663
|
+
* resolvePosixPath = resolvePathFactory(getCwd, custom_absolute_path_segment_tester)
|
|
1664
|
+
*
|
|
1665
|
+
* // aliasing our functions for brevity
|
|
1666
|
+
* const eq = assertEquals, fn = resolvePosixPath
|
|
1667
|
+
*
|
|
1668
|
+
* // relative path resolution
|
|
1669
|
+
* eq(fn("a", "b", "c.zip"), "x:/y/z/a/b/c.zip")
|
|
1670
|
+
* eq(fn(".\\a", "b", "c\\"), "x:/y/z/a/b/c/")
|
|
1671
|
+
* eq(fn("a/", "b/", "c/"), "x:/y/z/a/b/c/")
|
|
1672
|
+
* eq(fn("../a", "../b", "c\\"), "x:/b/c/")
|
|
1673
|
+
* eq(fn("../a/", "../b", "c/"), "x:/y/b/c/")
|
|
1674
|
+
* eq(fn("a", "b", "c.zip", "./"), "x:/y/z/a/b/")
|
|
1675
|
+
*
|
|
1676
|
+
* // pre-existing absolute path resolution
|
|
1677
|
+
* eq(fn("./a", "/b", "c:/"), "c:/") // both "/b" and "c:/" are absolute paths, and so they purge all segments behind them.
|
|
1678
|
+
* eq(fn("./a/", "\\b/", "./c"), "/b/c") // "\\b/" is an absolute path, hence it negates all segments prior to it.
|
|
1679
|
+
* eq(fn("file:///", "a/b/c", "./d"), "file:///a/b/d") // the first "file:///" segment is an absolute path according to our `custom_absolute_path_segment_tester`
|
|
1680
|
+
* eq(fn("file:/", "a/b/c", "./d"), "x:/y/z/file:/a/b/d") // "file:/" is not considered as an absolute path by `custom_absolute_path_segment_tester`, thus it the cwd will be prepended to the final path
|
|
1681
|
+
* eq(fn("file:", "a/b\\c", ".\\d"), "x:/y/z/file:/a/b/d") // same as before, but we're putting emphasis on the mandatory "/" separator that gets added after the "file:" segment
|
|
1682
|
+
* eq(fn("a/b/c", "http://d/e/f.txt"), "http://d/e/f.txt") // the "http://" segment is identified as an absolute path by our `custom_absolute_path_segment_tester`
|
|
1683
|
+
* ```
|
|
1684
|
+
*
|
|
1685
|
+
* for lots of other (posix-only) test cases, see the doc comments of {@link resolvePosixPathFactory}.
|
|
1686
|
+
*/
|
|
1687
|
+
export const resolvePathFactory = (absolute_current_dir, absolute_segment_test_fn = isAbsolutePath) => {
|
|
1688
|
+
if (isString(absolute_current_dir)) {
|
|
1689
|
+
absolute_current_dir = pathToPosixPath(absolute_current_dir);
|
|
1690
|
+
}
|
|
1691
|
+
const getCwdPath = isString(absolute_current_dir)
|
|
1692
|
+
? (() => absolute_current_dir)
|
|
1693
|
+
: (() => pathToPosixPath(absolute_current_dir())), posix_path_resolver = resolvePosixPathFactory(getCwdPath, absolute_segment_test_fn);
|
|
1694
|
+
return (...segments) => posix_path_resolver(...segments.map(pathToPosixPath));
|
|
1695
|
+
};
|
|
1696
|
+
const glob_pattern_to_regex_escape_control_chars = /[\.\+\^\$\{\}\(\)\|\[\]\\]/g, glob_starstar_wildcard_token = "<<<StarStarWildcard>>>";
|
|
1697
|
+
/** convert a glob string to a regex object.
|
|
1698
|
+
*
|
|
1699
|
+
* TODO: purge the info below:
|
|
1700
|
+
* in this implementation, only the wildcards `"*"`, `"**"`, and the optional `"?"` is given meaning.
|
|
1701
|
+
* all else, including parenthesis, brackets, dots, and backslash, are escaped when being converted into a regex.
|
|
1702
|
+
*
|
|
1703
|
+
* TODO: test it
|
|
1704
|
+
* TODO: also implement a `isGlobPattern` function
|
|
1705
|
+
* TODO: make it so that the user can configure which features to turn on or off
|
|
1706
|
+
*
|
|
1707
|
+
* @beta
|
|
1708
|
+
*/
|
|
1709
|
+
export const globPatternToRegex = (glob_pattern) => {
|
|
1710
|
+
const
|
|
1711
|
+
// first, convert windows path separator to posix path separator
|
|
1712
|
+
posix_pattern = pathToPosixPath(glob_pattern), regex_str = posix_pattern
|
|
1713
|
+
// escape regex special characters, except for "*", "?", "[", "]", "{", and "}"
|
|
1714
|
+
.replace(glob_pattern_to_regex_escape_control_chars, "\\$&")
|
|
1715
|
+
// replace "**/" or "**" directory wildcard with a temporary `glob_starstar_wildcard_token`, and later we will convert to ".*"
|
|
1716
|
+
.replace(/\*\*\/?/g, glob_starstar_wildcard_token)
|
|
1717
|
+
// replace single "*" with "[^/]*" to match anything except directory separators
|
|
1718
|
+
.replace(/\*/g, "[^\/]*")
|
|
1719
|
+
// replace "?" with "." to match any single character
|
|
1720
|
+
.replace(/\?/g, ".")
|
|
1721
|
+
// convert negated glob ranges (like "[!abc]") to regex negation ("[^abc]")
|
|
1722
|
+
.replace(/\[!(.*)\]/g, "[^$1]")
|
|
1723
|
+
// support for character ranges like "[a-z]"
|
|
1724
|
+
.replace(/\[(.*)\]/g, "[$1]")
|
|
1725
|
+
// support for braces like "{js,ts}"
|
|
1726
|
+
.replace(/\{([^,}]+),([^}]+)\}/g, "($1|$2)")
|
|
1727
|
+
// put back the ".*" wildcards where they belong
|
|
1728
|
+
.replace(glob_starstar_wildcard_token, ".*");
|
|
1729
|
+
return new RegExp("^" + regex_str + "$");
|
|
1730
|
+
};
|