• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

streetsidesoftware / cspell / 12339180630

15 Dec 2024 01:23PM UTC coverage: 94.057%. First build
12339180630

Pull #6671

github

web-flow
Merge 825fb3d46 into 2ed706e50
Pull Request #6671: feat: Support Windows UNC files.

14048 of 16216 branches covered (86.63%)

44 of 59 new or added lines in 13 files covered. (74.58%)

15147 of 16104 relevant lines covered (94.06%)

31160.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.24
/packages/cspell-url/src/FileUrlBuilder.mts
1
import assert from 'node:assert';
2
import Path from 'node:path';
3
import { pathToFileURL } from 'node:url';
4

5
import {
6
    isFileURL,
7
    isWindows,
8
    isWindowsFileUrl,
9
    isWindowsPathnameWithDriveLatter,
10
    pathWindowsDriveLetterToUpper,
11
    regExpWindowsPathDriveLetter,
12
    toFilePathOrHref,
13
} from './fileUrl.mjs';
14
import {
15
    addTrailingSlash,
16
    isUrlLike,
17
    normalizeWindowsUrl,
18
    regExpWindowsPath,
19
    urlParent,
20
    urlToUrlRelative,
21
} from './url.mjs';
22

23
const isWindowsPathRegEx = regExpWindowsPathDriveLetter;
3✔
24
const isWindowsPathname = regExpWindowsPath;
3✔
25
const startsWithSlash = /^[\\/]/;
3✔
26

27
export const percentRegEx = /%/g;
3✔
28
export const backslashRegEx = /\\/g;
3✔
29
export const newlineRegEx = /\n/g;
3✔
30
export const carriageReturnRegEx = /\r/g;
3✔
31
export const tabRegEx = /\t/g;
3✔
32
export const questionRegex = /\?/g;
3✔
33
export const hashRegex = /#/g;
3✔
34

35
export interface PathInterface {
36
    sep: string;
37
    resolve(...paths: string[]): string;
38
    parse(path: string): Path.ParsedPath;
39
    normalize(path: string): string;
40
    relative(from: string, to: string): string;
41
    isAbsolute(path: string): boolean;
42
}
43

44
export interface BuilderOptions {
45
    windows?: boolean | undefined;
46
    path?: PathInterface | undefined;
47
    cwd?: URL | undefined;
48
}
49

50
const ProtocolFile = 'file:';
3✔
51

52
export class FileUrlBuilder {
53
    private windows: boolean;
54
    readonly path: PathInterface;
55
    readonly cwd: URL;
56
    constructor(options: BuilderOptions = {}) {
14✔
57
        const sep = options.path?.sep;
47✔
58
        this.windows = options.windows ?? (sep ? sep === '\\' : undefined) ?? isWindows;
47✔
59
        this.path = options.path ?? (this.windows ? Path.win32 : Path.posix);
47✔
60
        // note: `this.path.resolve() + '/'` is used on purpose instead of `'./'`
61
        this.cwd = options.cwd ?? this.pathToFileURL(this.path.resolve() + '/', this.rootFileURL());
47✔
62
        assert(
47✔
63
            this.path.sep === (this.windows ? '\\' : '/'),
94✔
64
            `Path separator should match OS type Windows: ${this.windows === true ? 'true' : (this.windows ?? 'undefined') || 'false'}, ` +
162!
65
                `sep: ${this.path.sep}, ` +
66
                `options: ` +
67
                JSON.stringify({
68
                    isWindows,
69
                    sep: `${sep}`,
70
                    windows: options.windows,
71
                    pathSep: options.path?.sep,
72
                    n: options.path?.normalize('path/file.txt'),
73
                    cwd: options.cwd?.href,
74
                    win32: this.path === Path.win32,
75
                    posix: this.path === Path.posix,
76
                    'win32.normalize': this.path.normalize === Path.win32.normalize,
77
                    'posix.normalize': this.path.normalize === Path.posix.normalize,
78
                }) +
79
                ``,
80
        );
81
    }
82

83
    /**
84
     * Encode special characters in a file path to use in a URL.
85
     * @param filepath
86
     * @returns
87
     */
88
    encodePathChars(filepath: string) {
89
        filepath = filepath.replaceAll(percentRegEx, '%25');
167✔
90
        // In posix, backslash is a valid character in paths:
91
        if (!this.windows && !isWindows && filepath.includes('\\')) {
167!
92
            filepath = filepath.replaceAll(backslashRegEx, '%5C');
×
93
        }
94
        filepath = filepath.replaceAll(newlineRegEx, '%0A');
167✔
95
        filepath = filepath.replaceAll(carriageReturnRegEx, '%0D');
167✔
96
        filepath = filepath.replaceAll(tabRegEx, '%09');
167✔
97
        return filepath;
167✔
98
    }
99

100
    /**
101
     * Normalize a file path for use in a URL.
102
     * ```js
103
     * const url = new URL(normalizeFilePathForUrl('path\\to\\file.txt'), 'file:///Users/user/');
104
     * // Result: file:///Users/user/path/to/file.txt
105
     * ```
106
     * @param filePath
107
     * @returns a normalized file path for use as a relative path in a URL.
108
     */
109
    normalizeFilePathForUrl(filePath: string): string {
110
        filePath = this.encodePathChars(filePath);
167✔
111
        filePath = filePath.replaceAll(questionRegex, '%3F');
167✔
112
        filePath = filePath.replaceAll(hashRegex, '%23');
167✔
113
        const pathname = filePath.replaceAll('\\', '/');
167✔
114
        return pathname.replace(isWindowsPathRegEx, (drive) => `/${drive}`.toUpperCase());
167✔
115
    }
116

117
    /**
118
     * Try to make a file URL.
119
     * - if filenameOrUrl is already a URL, it is returned as is.
120
     * @param filenameOrUrl
121
     * @param relativeTo - optional URL, if given, filenameOrUrl will be parsed as relative.
122
     * @returns a URL
123
     */
124
    toFileURL(filenameOrUrl: string | URL, relativeTo?: string | URL): URL {
125
        return normalizeWindowsUrl(this.#toFileURL(filenameOrUrl, relativeTo));
33✔
126
    }
127

128
    /**
129
     * Try to make a file URL.
130
     * - if filenameOrUrl is already a URL, it is returned as is.
131
     * @param filenameOrUrl
132
     * @param relativeTo - optional URL, if given, filenameOrUrl will be parsed as relative.
133
     * @returns a URL
134
     */
135
    #toFileURL(filenameOrUrl: string | URL, relativeTo?: string | URL): URL {
136
        if (typeof filenameOrUrl !== 'string') return filenameOrUrl;
33✔
137
        if (isUrlLike(filenameOrUrl)) return normalizeWindowsUrl(new URL(filenameOrUrl));
28✔
138
        relativeTo ??= this.cwd;
18✔
139
        isWindows && (filenameOrUrl = filenameOrUrl.replaceAll('\\', '/'));
18!
140
        if (this.isAbsolute(filenameOrUrl) && isFileURL(relativeTo)) {
18✔
141
            const pathname = this.normalizeFilePathForUrl(filenameOrUrl);
6✔
142
            if (isWindowsFileUrl(relativeTo) && !isWindowsPathnameWithDriveLatter(pathname)) {
6!
NEW
143
                const relFilePrefix = relativeTo.toString().slice(0, 10);
×
NEW
144
                return normalizeWindowsUrl(new URL(relFilePrefix + pathname));
×
145
            }
146
            return normalizeWindowsUrl(new URL('file://' + pathname));
6✔
147
        }
148
        if (isUrlLike(relativeTo)) {
12✔
149
            const pathname = this.normalizeFilePathForUrl(filenameOrUrl);
9✔
150
            return normalizeWindowsUrl(new URL(pathname, relativeTo));
9✔
151
        }
152
        // Resolve removes the trailing slash, so we need to add it back.
153
        const appendSlash = filenameOrUrl.endsWith('/') ? '/' : '';
3!
154
        const pathname =
155
            this.normalizeFilePathForUrl(this.path.resolve(relativeTo.toString(), filenameOrUrl)) + appendSlash;
3✔
156
        return normalizeWindowsUrl(new URL('file://' + pathname));
3✔
157
    }
158

159
    /**
160
     * Try to make a URL for a directory.
161
     * - if dirOrUrl is already a URL, a slash is appended to the pathname.
162
     * @param dirOrUrl - directory path to convert to a file URL.
163
     * @param relativeTo - optional URL, if given, filenameOrUrl will be parsed as relative.
164
     * @returns a URL
165
     */
166
    toFileDirURL(dirOrUrl: string | URL, relativeTo?: string | URL): URL {
167
        return addTrailingSlash(this.toFileURL(dirOrUrl, relativeTo));
5✔
168
    }
169

170
    urlToFilePathOrHref(url: URL | string): string {
171
        url = this.toFileURL(url);
5✔
172
        return this.#urlToFilePathOrHref(url);
5✔
173
    }
174

175
    #urlToFilePathOrHref(url: URL): string {
176
        if (url.protocol !== ProtocolFile || url.hostname) return url.href;
9✔
177
        const p =
178
            this.path === Path
8✔
179
                ? toFilePathOrHref(url)
180
                : decodeURIComponent(url.pathname.split('/').join(this.path.sep));
181
        return pathWindowsDriveLetterToUpper(p.replace(isWindowsPathname, '$1'));
8✔
182
    }
183

184
    /**
185
     * Calculate the relative path to go from `urlFrom` to `urlTo`.
186
     * The protocol is not evaluated. Only the `url.pathname` is used.
187
     * The result: `new URL(relative(urlFrom, urlTo), urlFrom).pathname === urlTo.pathname`
188
     * @param urlFrom
189
     * @param urlTo
190
     * @returns the relative path
191
     */
192
    relative(urlFrom: URL, urlTo: URL): string {
193
        if (urlFrom.protocol === urlTo.protocol && urlFrom.protocol === ProtocolFile) {
4!
194
            if (urlFrom.href === urlTo.href) return '';
4✔
195
            urlFrom = urlFrom.pathname.endsWith('/') ? urlFrom : new URL('./', urlFrom);
3✔
196
            const fromPath = urlFrom.pathname;
3✔
197
            const toPath = urlTo.pathname;
3✔
198
            if (toPath.startsWith(fromPath)) return decodeURIComponent(toPath.slice(fromPath.length));
3✔
199
            const pFrom = this.#urlToFilePathOrHref(urlFrom);
2✔
200
            const pTo = this.#urlToFilePathOrHref(urlTo);
2✔
201
            const toIsDir = urlTo.pathname.endsWith('/');
2✔
202
            let pathname = this.normalizeFilePathForUrl(this.path.relative(pFrom, pTo));
2✔
203
            if (toIsDir && !pathname.endsWith('/')) pathname += '/';
2!
204
            return decodeURIComponent(pathname);
2✔
205
        }
206
        return decodeURIComponent(urlToUrlRelative(urlFrom, urlTo));
×
207
    }
208

209
    /**
210
     * Get the parent directory of a URL.
211
     * @param url
212
     */
213
    urlDirname(url: URL | string): URL {
214
        return urlParent(this.toFileURL(url));
×
215
    }
216

217
    pathToFileURL(pathname: string, relativeToURL?: URL | string): URL {
218
        return new URL(this.normalizeFilePathForUrl(pathname), relativeToURL || this.cwd);
55✔
219
    }
220

221
    rootFileURL(filePath?: string): URL {
222
        const path = this.path;
47✔
223
        const p = path.parse(path.normalize(path.resolve(filePath ?? '.')));
47✔
224
        return new URL(this.normalizeFilePathForUrl(p.root), this.#getFsRootURL());
47✔
225
    }
226

227
    #getFsRootURL() {
228
        if (this.path === Path) return pathToFileURL('/');
47✔
229
        const p = this.path.resolve('/');
30✔
230
        return new URL(this.normalizeFilePathForUrl(p), 'file:///');
30✔
231
    }
232

233
    /**
234
     * Determine if a filePath is absolute.
235
     *
236
     * @param filePath
237
     * @returns true if `URL` or `path.isAbsolute(filePath)`
238
     */
239
    isAbsolute(filePath: string): boolean {
240
        return isUrlLike(filePath) || this.path.isAbsolute(filePath);
28✔
241
    }
242

243
    isUrlLike(url: string | URL): boolean {
244
        return isUrlLike(url);
6✔
245
    }
246
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc