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

streetsidesoftware / cspell / 12339229580

15 Dec 2024 01:30PM UTC coverage: 94.063%. First build
12339229580

Pull #6671

github

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

14052 of 16216 branches covered (86.66%)

43 of 58 new or added lines in 13 files covered. (74.14%)

15147 of 16103 relevant lines covered (94.06%)

31099.98 hits per line

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

93.15
/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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

242
    isUrlLike(url: string | URL): boolean {
243
        return isUrlLike(url);
6✔
244
    }
245
}
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