UNPKG

typescript

Version:

TypeScript is a language for application scale JavaScript development

508 lines • 19.6 kB
const CharacterCodesSlash = "/".charCodeAt(0); const CharacterCodesBackslash = "\\".charCodeAt(0); const CharacterCodesColon = ":".charCodeAt(0); const CharacterCodesPercent = "%".charCodeAt(0); const CharacterCodes3 = "3".charCodeAt(0); const CharacterCodesa = "a".charCodeAt(0); const CharacterCodesz = "z".charCodeAt(0); const CharacterCodesA = "A".charCodeAt(0); const CharacterCodesZ = "Z".charCodeAt(0); const CharacterCodesDot = ".".charCodeAt(0); const directorySeparator = "/"; const altDirectorySeparator = "\\"; const urlSchemeSeparator = "://"; const backslashRegExp = /\\/g; // check path for these segments: '', '.'. '..' const relativePathSegmentRegExp = /\/\/|(?:^|\/)\.\.?(?:$|\/)/; /** * Determines whether a charCode corresponds to `/` or `\\`. */ function isAnyDirectorySeparator(charCode) { return charCode === CharacterCodesSlash || charCode === CharacterCodesBackslash; } function isVolumeCharacter(charCode) { return (charCode >= CharacterCodesa && charCode <= CharacterCodesz) || (charCode >= CharacterCodesA && charCode <= CharacterCodesZ); } function getFileUrlVolumeSeparatorEnd(url, start) { const ch0 = url.charCodeAt(start); if (ch0 === CharacterCodesColon) return start + 1; if (ch0 === CharacterCodesPercent && url.charCodeAt(start + 1) === CharacterCodes3) { const ch2 = url.charCodeAt(start + 2); if (ch2 === CharacterCodesa || ch2 === CharacterCodesA) return start + 3; } return -1; } /** * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files"). * * For example: * ```ts * getRootLength("a") === 0 // "" * getRootLength("/") === 1 // "/" * getRootLength("c:") === 2 // "c:" * getRootLength("c:d") === 0 // "" * getRootLength("c:/") === 3 // "c:/" * getRootLength("c:\\") === 3 // "c:\\" * getRootLength("//server") === 7 // "//server" * getRootLength("//server/share") === 8 // "//server/" * getRootLength("\\\\server") === 7 // "\\\\server" * getRootLength("\\\\server\\share") === 8 // "\\\\server\\" * getRootLength("file:///path") === 8 // "file:///" * getRootLength("file:///c:") === 10 // "file:///c:" * getRootLength("file:///c:d") === 8 // "file:///" * getRootLength("file:///c:/path") === 11 // "file:///c:/" * getRootLength("file://server") === 13 // "file://server" * getRootLength("file://server/path") === 14 // "file://server/" * getRootLength("http://server") === 13 // "http://server" * getRootLength("http://server/path") === 14 // "http://server/" * ``` * * @internal */ export function getRootLength(path) { const rootLength = getEncodedRootLength(path); return rootLength < 0 ? ~rootLength : rootLength; } /** * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files"). * If the root is part of a URL, the twos-complement of the root length is returned. */ function getEncodedRootLength(path) { if (!path) return 0; const ch0 = path.charCodeAt(0); // POSIX or UNC if (ch0 === CharacterCodesSlash || ch0 === CharacterCodesBackslash) { if (path.charCodeAt(1) !== ch0) return 1; // POSIX: "/" (or non-normalized "\") const p1 = path.indexOf(ch0 === CharacterCodesSlash ? directorySeparator : altDirectorySeparator, 2); if (p1 < 0) return path.length; // UNC: "//server" or "\\server" return p1 + 1; // UNC: "//server/" or "\\server\" } // DOS if (isVolumeCharacter(ch0) && path.charCodeAt(1) === CharacterCodesColon) { const ch2 = path.charCodeAt(2); if (ch2 === CharacterCodesSlash || ch2 === CharacterCodesBackslash) return 3; // DOS: "c:/" or "c:\" if (path.length === 2) return 2; // DOS: "c:" (but not "c:d") } // URL const schemeEnd = path.indexOf(urlSchemeSeparator); if (schemeEnd !== -1) { const authorityStart = schemeEnd + urlSchemeSeparator.length; const authorityEnd = path.indexOf(directorySeparator, authorityStart); if (authorityEnd !== -1) { // URL: "file:///", "file://server/", "file://server/path" // For local "file" URLs, include the leading DOS volume (if present). // Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a // special case interpreted as "the machine from which the URL is being interpreted". const scheme = path.slice(0, schemeEnd); const authority = path.slice(authorityStart, authorityEnd); if (scheme === "file" && (authority === "" || authority === "localhost") && isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) { const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2); if (volumeSeparatorEnd !== -1) { if (path.charCodeAt(volumeSeparatorEnd) === CharacterCodesSlash) { // URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/" return ~(volumeSeparatorEnd + 1); } if (volumeSeparatorEnd === path.length) { // URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a" // but not "file:///c:d" or "file:///c%3ad" return ~volumeSeparatorEnd; } } } return ~(authorityEnd + 1); // URL: "file://server/", "http://server/" } return ~path.length; // URL: "file://server", "http://server" } // relative return 0; } export function getPathComponents(path) { return pathComponents(path, getRootLength(path)); } function pathComponents(path, rootLength) { const root = path.substring(0, rootLength); const rest = path.substring(rootLength).split("/"); if (rest.length && !lastOrUndefined(rest)) rest.pop(); return [root, ...rest]; } function lastOrUndefined(array) { return array.length ? array[array.length - 1] : undefined; } /** * Determines whether a path has a trailing separator (`/` or `\\`). */ export function hasTrailingDirectorySeparator(path) { return path.length > 0 && isAnyDirectorySeparator(path.charCodeAt(path.length - 1)); } /** * Removes a trailing directory separator from a path, if it does not already have one. */ export function removeTrailingDirectorySeparator(path) { if (hasTrailingDirectorySeparator(path)) { return path.substr(0, path.length - 1); } return path; } /** * Adds a trailing directory separator to a path, if it does not already have one. */ export function ensureTrailingDirectorySeparator(path) { if (!hasTrailingDirectorySeparator(path)) { return path + directorySeparator; } return path; } /** * Normalize path separators, converting `\\` into `/`. */ export function normalizeSlashes(path) { return path.includes("\\") ? path.replace(backslashRegExp, directorySeparator) : path; } /** * Combines paths. If a path is absolute, it replaces any previous path. Relative paths are not simplified. */ export function combinePaths(path, ...paths) { if (path) path = normalizeSlashes(path); for (let relativePath of paths) { if (!relativePath) continue; relativePath = normalizeSlashes(relativePath); if (!path || getRootLength(relativePath) !== 0) { path = relativePath; } else { path = ensureTrailingDirectorySeparator(path) + relativePath; } } return path; } function simpleNormalizePath(path) { // Most paths don't require normalization if (!relativePathSegmentRegExp.test(path)) { return path; } // Some paths only require cleanup of `/./` or leading `./` let simplified = path.replace(/\/\.\//g, "/"); if (simplified.startsWith("./")) { simplified = simplified.slice(2); } if (simplified !== path) { path = simplified; if (!relativePathSegmentRegExp.test(path)) { return path; } } return undefined; } /** * Returns the normalized absolute path, resolving `.` and `..` segments. */ export function getNormalizedAbsolutePath(path, currentDirectory) { let rootLength = getRootLength(path); if (rootLength === 0 && currentDirectory) { path = combinePaths(currentDirectory, path); rootLength = getRootLength(path); } else { // combinePaths normalizes slashes, so not necessary in the other branch path = normalizeSlashes(path); } const simpleNormalized = simpleNormalizePath(path); if (simpleNormalized !== undefined) { return simpleNormalized.length > rootLength ? removeTrailingDirectorySeparator(simpleNormalized) : simpleNormalized; } const length = path.length; const root = path.substring(0, rootLength); // `normalized` is only initialized once `path` is determined to be non-normalized let normalized; let index = rootLength; let segmentStart = index; let normalizedUpTo = index; let seenNonDotDotSegment = rootLength !== 0; while (index < length) { // At beginning of segment segmentStart = index; let ch = path.charCodeAt(index); while (ch === CharacterCodesSlash && index + 1 < length) { index++; ch = path.charCodeAt(index); } if (index > segmentStart) { // Seen superfluous separator normalized ??= path.substring(0, segmentStart - 1); segmentStart = index; } // Past any superfluous separators let segmentEnd = path.indexOf(directorySeparator, index + 1); if (segmentEnd === -1) { segmentEnd = length; } const segmentLength = segmentEnd - segmentStart; if (segmentLength === 1 && path.charCodeAt(index) === CharacterCodesDot) { // "." segment (skip) normalized ??= path.substring(0, normalizedUpTo); } else if (segmentLength === 2 && path.charCodeAt(index) === CharacterCodesDot && path.charCodeAt(index + 1) === CharacterCodesDot) { // ".." segment if (!seenNonDotDotSegment) { if (normalized !== undefined) { normalized += normalized.length === rootLength ? ".." : "/.."; } else { normalizedUpTo = index + 2; } } else if (normalized === undefined) { if (normalizedUpTo - 2 >= 0) { normalized = path.substring(0, Math.max(rootLength, path.lastIndexOf(directorySeparator, normalizedUpTo - 2))); } else { normalized = path.substring(0, normalizedUpTo); } } else { const lastSlash = normalized.lastIndexOf(directorySeparator); if (lastSlash !== -1) { normalized = normalized.substring(0, Math.max(rootLength, lastSlash)); } else { normalized = root; } if (normalized.length === rootLength) { seenNonDotDotSegment = rootLength !== 0; } } } else if (normalized !== undefined) { if (normalized.length !== rootLength) { normalized += directorySeparator; } seenNonDotDotSegment = true; normalized += path.substring(segmentStart, segmentEnd); } else { seenNonDotDotSegment = true; normalizedUpTo = segmentEnd; } index = segmentEnd + 1; } return normalized ?? (length > rootLength ? removeTrailingDirectorySeparator(path) : path); } /** * Normalizes a path, resolving `.` and `..` segments and converting backslashes to forward slashes. */ export function normalizePath(path) { path = normalizeSlashes(path); let normalized = simpleNormalizePath(path); if (normalized !== undefined) { return normalized; } normalized = getNormalizedAbsolutePath(path, ""); return normalized && hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalized) : normalized; } /** * Determines whether a path is an absolute disk path (e.g. starts with `/`, or a DOS path * like `c:`, `c:\\` or `c:/`). */ export function isRootedDiskPath(path) { return getEncodedRootLength(path) > 0; } /** * Converts a file name to a normalized path. * * @param fileName The file name to convert * @param basePath The base path to use for relative file names * @param getCanonicalFileName A function to get the canonical file name (e.g., toLowerCase for case-insensitive systems) * @returns The normalized path */ export function toPath(fileName, basePath, getCanonicalFileName) { const nonCanonicalizedPath = isRootedDiskPath(fileName) ? normalizePath(fileName) : getNormalizedAbsolutePath(fileName, basePath); return getCanonicalFileName(nonCanonicalizedPath); } /** * Creates a getCanonicalFileName function based on case sensitivity. */ export function createGetCanonicalFileName(useCaseSensitiveFileNames) { return useCaseSensitiveFileNames ? identity : toLowerCase; } function identity(x) { return x; } function toLowerCase(s) { return s.toLowerCase(); } const bundledScheme = "bundled:///"; /** * Returns true if the path refers to a bundled library file. */ export function isBundled(path) { return path.startsWith(bundledScheme); } /** * Returns true if the file name represents a dynamic/virtual file * that doesn't exist on disk (e.g., untitled files with paths like "^/untitled/..."). */ export function isDynamicFileName(fileName) { return fileName.startsWith("^/"); } /** * Splits a Windows volume (e.g., "c:") from the rest of the path. * Returns [volume, rest, ok] where ok is true if a volume was found. */ export function splitVolumePath(path) { if (path.length >= 2 && isVolumeCharacter(path.charCodeAt(0)) && path.charCodeAt(1) === CharacterCodesColon) { return [path.substring(0, 2).toLowerCase(), path.substring(2), true]; } return ["", path, false]; } // Characters that need extra escaping in URI path segments // https://github.com/microsoft/vscode-uri/blob/edfdccd976efaf4bb8fdeca87e97c47257721729/src/uri.ts#L455 const extraEscapeChars = { ":": "%3A", "/": "%2F", "?": "%3F", "#": "%23", "[": "%5B", "]": "%5D", "@": "%40", "!": "%21", "$": "%24", "&": "%26", "'": "%27", "(": "%28", ")": "%29", "*": "%2A", "+": "%2B", ",": "%2C", ";": "%3B", "=": "%3D", " ": "%20", }; function extraEscape(s) { let result = s; for (const [char, escape] of Object.entries(extraEscapeChars)) { result = result.replaceAll(char, escape); } return result; } /** * Converts a file name to a document URI. * * @example * fileNameToDocumentURI("/path/to/file.ts") === "file:///path/to/file.ts" * fileNameToDocumentURI("c:/path/to/file.ts") === "file:///c%3A/path/to/file.ts" * fileNameToDocumentURI("^/untitled/ts-nul-authority/Untitled-1") === "untitled:Untitled-1" * fileNameToDocumentURI("^/vscode-vfs/github/microsoft/typescript-go/file.ts") === "vscode-vfs://github/microsoft/typescript-go/file.ts" */ export function fileNameToDocumentURI(fileName) { // Bundled files are returned as-is if (isBundled(fileName)) { return fileName; } // Dynamic/virtual files (untitled, vscode-vfs, etc.) need special handling if (isDynamicFileName(fileName)) { // Format: ^/scheme/authority/path const withoutPrefix = fileName.substring(2); // Remove "^/" const firstSlash = withoutPrefix.indexOf("/"); if (firstSlash === -1) { throw new Error("invalid file name: " + fileName); } const scheme = withoutPrefix.substring(0, firstSlash); const rest = withoutPrefix.substring(firstSlash + 1); const secondSlash = rest.indexOf("/"); if (secondSlash === -1) { throw new Error("invalid file name: " + fileName); } const authority = rest.substring(0, secondSlash); const path = rest.substring(secondSlash + 1); // ts-nul-authority is a placeholder for URIs without an authority if (authority === "ts-nul-authority") { return scheme + ":" + path; } return scheme + "://" + authority + "/" + path; } // Regular file path - convert to file:// URI let [volume, rest] = splitVolumePath(fileName); if (volume !== "") { volume = "/" + extraEscape(volume); } // Remove leading // for UNC paths (already handled by file://) if (rest.startsWith("//")) { rest = rest.substring(2); } const parts = rest.split("/"); const encodedParts = parts.map(part => extraEscape(encodeURIComponent(part))); return "file://" + volume + encodedParts.join("/"); } /** * Converts a document URI to a file name. * * @example * documentURIToFileName("file:///path/to/file.ts") === "/path/to/file.ts" * documentURIToFileName("file:///c%3A/path/to/file.ts") === "c:/path/to/file.ts" * documentURIToFileName("untitled:Untitled-1") === "^/untitled/ts-nul-authority/Untitled-1" * documentURIToFileName("vscode-vfs://github/microsoft/typescript-go/file.ts") === "^/vscode-vfs/github/microsoft/typescript-go/file.ts" */ export function documentURIToFileName(uri) { // Bundled files are returned as-is if (isBundled(uri)) { return uri; } // Handle file:// URIs if (uri.startsWith("file://")) { let parsed; try { parsed = new URL(uri); } catch { throw new Error("invalid file URI: " + uri); } // UNC path: file://server/share/... if (parsed.host !== "") { return "//" + parsed.host + parsed.pathname; } // Local file - fix Windows path by removing leading slash before volume const path = decodeURIComponent(parsed.pathname); if (path.length >= 3 && path.charCodeAt(0) === CharacterCodesSlash) { const [volume, rest, ok] = splitVolumePath(path.substring(1)); if (ok) { return volume + rest; } } return path; } // Leave all other URIs escaped so we can round-trip them. // Convert to dynamic file name format: ^/scheme/authority/path const colonIndex = uri.indexOf(":"); if (colonIndex === -1) { throw new Error("invalid URI: " + uri); } const scheme = uri.substring(0, colonIndex); let path = uri.substring(colonIndex + 1); let authority = "ts-nul-authority"; if (path.startsWith("//")) { const rest = path.substring(2); const slashIndex = rest.indexOf("/"); if (slashIndex === -1) { throw new Error("invalid URI: " + uri); } authority = rest.substring(0, slashIndex); path = rest.substring(slashIndex + 1); } return "^/" + scheme + "/" + authority + "/" + path; } //# sourceMappingURL=path.js.map