"use strict"; /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.hasSSHProtocol = exports.buildHttpsUrl = exports.buildSshUrl = exports.removeTrailingSlash = exports.addTrailingSlash = exports.addLeadingSlash = exports.resolvePathname = exports.serializeURLPath = exports.parseURLPath = exports.isValidPathname = exports.encodePath = exports.fileToPath = exports.getEditUrl = exports.normalizeUrl = void 0; const tslib_1 = require("tslib"); const resolve_pathname_1 = tslib_1.__importDefault(require("resolve-pathname")); const jsUtils_1 = require("./jsUtils"); /** * Much like `path.join`, but much better. Takes an array of URL segments, and * joins them into a reasonable URL. * * - `["file:", "/home", "/user/", "website"]` => `file:///home/user/website` * - `["file://", "home", "/user/", "website"]` => `file://home/user/website` (relative!) * - Remove trailing slash before parameters or hash. * - Replace `?` in query parameters with `&`. * - Dedupe forward slashes in the entire path, avoiding protocol slashes. * * @throws {TypeError} If any of the URL segment is not a string, this throws. */ function normalizeUrl(rawUrls) { const urls = [...rawUrls]; const resultArray = []; let hasStartingSlash = false; let hasEndingSlash = false; const isNonEmptyArray = (arr) => arr.length > 0; if (!isNonEmptyArray(urls)) { return ''; } // If the first part is a plain protocol, we combine it with the next part. if (urls[0].match(/^[^/:]+:\/*$/) && urls.length > 1) { const first = urls.shift(); if (first.startsWith('file:') && urls[0].startsWith('/')) { // Force a double slash here, else we lose the information that the next // segment is an absolute path urls[0] = `${first}//${urls[0]}`; } else { urls[0] = first + urls[0]; } } // There must be two or three slashes in the file protocol, // two slashes in anything else. const replacement = urls[0].match(/^file:\/\/\//) ? '$1:///' : '$1://'; urls[0] = urls[0].replace(/^(?[^/:]+):\/*/, replacement); for (let i = 0; i < urls.length; i += 1) { let component = urls[i]; if (typeof component !== 'string') { throw new TypeError(`Url must be a string. Received ${typeof component}`); } if (component === '') { if (i === urls.length - 1 && hasEndingSlash) { resultArray.push('/'); } continue; } if (component !== '/') { if (i > 0) { // Removing the starting slashes for each component but the first. component = component.replace(/^\/+/, // Special case where the first element of rawUrls is empty // ["", "/hello"] => /hello component.startsWith('/') && !hasStartingSlash ? '/' : ''); } hasEndingSlash = component.endsWith('/'); // Removing the ending slashes for each component but the last. For the // last component we will combine multiple slashes to a single one. component = component.replace(/\/+$/, i < urls.length - 1 ? '' : '/'); } hasStartingSlash = true; resultArray.push(component); } let str = resultArray.join('/'); // Each input component is now separated by a single slash except the possible // first plain protocol part. // Remove trailing slash before parameters or hash. str = str.replace(/\/(?\?|&|#[^!])/g, '$1'); // Replace ? in parameters with &. const parts = str.split('?'); str = parts.shift() + (parts.length > 0 ? '?' : '') + parts.join('&'); // Dedupe forward slashes in the entire path, avoiding protocol slashes. str = str.replace(/(?[^:/]\/)\/+/g, '$1'); // Dedupe forward slashes at the beginning of the path. str = str.replace(/^\/+/g, '/'); return str; } exports.normalizeUrl = normalizeUrl; /** * Takes a file's path, relative to its content folder, and computes its edit * URL. If `editUrl` is `undefined`, this returns `undefined`, as is the case * when the user doesn't want an edit URL in her config. */ function getEditUrl(fileRelativePath, editUrl) { return editUrl ? // Don't use posixPath for this: we need to force a forward slash path normalizeUrl([editUrl, fileRelativePath.replace(/\\/g, '/')]) : undefined; } exports.getEditUrl = getEditUrl; /** * Converts file path to a reasonable URL path, e.g. `'index.md'` -> `'/'`, * `'foo/bar.js'` -> `'/foo/bar'` */ function fileToPath(file) { const indexRE = /(?^|.*\/)index\.(?:mdx?|jsx?|tsx?)$/i; const extRE = /\.(?:mdx?|jsx?|tsx?)$/; if (indexRE.test(file)) { return file.replace(indexRE, '/$1'); } return `/${file.replace(extRE, '').replace(/\\/g, '/')}`; } exports.fileToPath = fileToPath; /** * Similar to `encodeURI`, but uses `encodeURIComponent` and assumes there's no * query. * * `encodeURI("/question?/answer")` => `"/question?/answer#section"`; * `encodePath("/question?/answer#section")` => `"/question%3F/answer%23foo"` */ function encodePath(userPath) { return userPath .split('/') .map((item) => encodeURIComponent(item)) .join('/'); } exports.encodePath = encodePath; /** * Whether `str` is a valid pathname. It must be absolute, and not contain * special characters. */ function isValidPathname(str) { if (!str.startsWith('/')) { return false; } try { const parsedPathname = new URL(str, 'https://domain.com').pathname; return parsedPathname === str || parsedPathname === encodeURI(str); } catch { return false; } } exports.isValidPathname = isValidPathname; // Let's name the concept of (pathname + search + hash) as URLPath // See also https://twitter.com/kettanaito/status/1741768992866308120 // Note: this function also resolves relative pathnames while parsing! function parseURLPath(urlPath, fromPath) { function parseURL(url, base) { try { // A possible alternative? https://github.com/unjs/ufo#url return new URL(url, base ?? 'https://example.com'); } catch (e) { throw new Error(`Can't parse URL ${url}${base ? ` with base ${base}` : ''}`, { cause: e }); } } const base = fromPath ? parseURL(fromPath) : undefined; const url = parseURL(urlPath, base); const { pathname } = url; // Fixes annoying url.search behavior // "" => undefined // "?" => "" // "?param => "param" const search = url.search ? url.search.slice(1) : urlPath.includes('?') ? '' : undefined; // Fixes annoying url.hash behavior // "" => undefined // "#" => "" // "?param => "param" const hash = url.hash ? url.hash.slice(1) : urlPath.includes('#') ? '' : undefined; return { pathname, search, hash, }; } exports.parseURLPath = parseURLPath; function serializeURLPath(urlPath) { const search = urlPath.search === undefined ? '' : `?${urlPath.search}`; const hash = urlPath.hash === undefined ? '' : `#${urlPath.hash}`; return `${urlPath.pathname}${search}${hash}`; } exports.serializeURLPath = serializeURLPath; /** * Resolve pathnames and fail-fast if resolution fails. Uses standard URL * semantics (provided by `resolve-pathname` which is used internally by React * router) */ function resolvePathname(to, from) { // TODO do we really need resolve-pathname lib anymore? // possible alternative: decodeURI(parseURLPath(to, from).pathname); return (0, resolve_pathname_1.default)(to, from); } exports.resolvePathname = resolvePathname; /** Appends a leading slash to `str`, if one doesn't exist. */ function addLeadingSlash(str) { return (0, jsUtils_1.addPrefix)(str, '/'); } exports.addLeadingSlash = addLeadingSlash; // TODO deduplicate: also present in @docusaurus/utils-common /** Appends a trailing slash to `str`, if one doesn't exist. */ function addTrailingSlash(str) { return (0, jsUtils_1.addSuffix)(str, '/'); } exports.addTrailingSlash = addTrailingSlash; /** Removes the trailing slash from `str`. */ function removeTrailingSlash(str) { return (0, jsUtils_1.removeSuffix)(str, '/'); } exports.removeTrailingSlash = removeTrailingSlash; /** Constructs an SSH URL that can be used to push to GitHub. */ function buildSshUrl(githubHost, organizationName, projectName, githubPort) { if (githubPort) { return `ssh://git@${githubHost}:${githubPort}/${organizationName}/${projectName}.git`; } return `git@${githubHost}:${organizationName}/${projectName}.git`; } exports.buildSshUrl = buildSshUrl; /** Constructs an HTTP URL that can be used to push to GitHub. */ function buildHttpsUrl(gitCredentials, githubHost, organizationName, projectName, githubPort) { if (githubPort) { return `https://${gitCredentials}@${githubHost}:${githubPort}/${organizationName}/${projectName}.git`; } return `https://${gitCredentials}@${githubHost}/${organizationName}/${projectName}.git`; } exports.buildHttpsUrl = buildHttpsUrl; /** * Whether the current URL is an SSH protocol. In addition to looking for * `ssh:`, it will also allow protocol-less URLs like * `git@github.com:facebook/docusaurus.git`. */ function hasSSHProtocol(sourceRepoUrl) { try { if (new URL(sourceRepoUrl).protocol === 'ssh:') { return true; } return false; } catch { // Fails when there isn't a protocol return /^(?:[\w-]+@)?[\w.-]+:[\w./-]+/.test(sourceRepoUrl); } } exports.hasSSHProtocol = hasSSHProtocol; //# sourceMappingURL=urlUtils.js.map