text: export const DASH_SCOPE = '-';
  
  export const PATH_SEPARATOR = '/';
  const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`);
  const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`);
  const SHA_REGEX = /[\da-f]{40}/gi;
  
  // About GitLab default host (overwrite in jh)
  export const PROMO_HOST = 'about.gitlab.com';
  
  // About Gitlab default url (overwrite in jh)
  export const PROMO_URL = `https://${PROMO_HOST}`;
  
  // Reset the cursor in a Regex so that multiple uses before a recompile don't fail
  function resetRegExp(regex) {
    regex.lastIndex = 0; /* eslint-disable-line no-param-reassign */
  
    return regex;
  }
  
  /**
   * Returns the absolute pathname for a relative or absolute URL string.
   *
   * A few examples of inputs and outputs:
   * 1) 'http://a.com/b/c/d' => '/b/c/d'
   * 2) '/b/c/d' => '/b/c/d'
   * 3) 'b/c/d' => '/b/c/d' or '[path]/b/c/d' depending of the current path of the
   *    document.location
   */
  export const parseUrlPathname = (url) => {
    const { pathname } = new URL(url, document.location.href);
    return pathname;
  };
  
  // Returns a decoded url parameter value
  // - Treats '+' as '%20'
  function decodeUrlParameter(val) {
    return decodeURIComponent(val.replace(/\+/g, '%20'));
  }
  
  /**
   * Safely encodes a string to be used as a path
   *
   * Note: This function DOES encode typical URL parts like ?, =, &, #, and +
   * If you need to use search parameters or URL fragments, they should be
   *     added AFTER calling this function, not before.
   *
   * Usecase: An image filename is stored verbatim, and you need to load it in
   *     the browser.
   *
   * Example: /some_path/file #1.jpg      ==> /some_path/file%20%231.jpg
   * Example: /some-path/file! Final!.jpg ==> /some-path/file%21%20Final%21.jpg
   *
   * Essentially, if a character *could* present a problem in a URL, it's escaped
   *     to the hexadecimal representation instead. This means it's a bit more
   *     aggressive than encodeURIComponent: that built-in function doesn't
   *     encode some characters that *could* be problematic, so this function
   *     adds them (#, !, ~, *, ', (, and )).
   *     Additionally, encodeURIComponent *does* encode `/`, but we want safer
   *     URLs, not non-functional URLs, so this function DEcodes slashes ('%2F').
   *
   * @param {String} potentiallyUnsafePath
   * @returns {String}
   */
  export function encodeSaferUrl(potentiallyUnsafePath) {
    const unencode = ['%2F'];
    const encode = ['#', '!', '~', '\\*', "'", '\\(', '\\)'];
    let saferPath = encodeURIComponent(potentiallyUnsafePath);
  
    unencode.forEach((code) => {
      saferPath = saferPath.replace(new RegExp(code, 'g'), decodeURIComponent(code));
    });
    encode.forEach((code) => {
      const encodedValue = code
        .codePointAt(code.length - 1)
        .toString(16)
        .toUpperCase();
  
      saferPath = saferPath.replace(new RegExp(code, 'g'), `%${encodedValue}`);
    });
  
    return saferPath;
  }
  
  export function cleanLeadingSeparator(path) {
    return path.replace(PATH_SEPARATOR_LEADING_REGEX, '');
  }
  
  function cleanEndingSeparator(path) {
    return path.replace(PATH_SEPARATOR_ENDING_REGEX, '');
  }
  
  /**
   * Safely joins the given paths which might both start and end with a `/`
   *
   * Example:
   * - `joinPaths('abc/', '/def') === 'abc/def'`
   * - `joinPaths(null, 'abc/def', 'zoo) === 'abc/def/zoo'`
   *
   * @param  {...String} paths
   * @returns {String}
   */
  export function joinPaths(...paths) {
    return paths.reduce((acc, path) => {
      if (!path) {
        return acc;
      }
      if (!acc) {
        return path;
      }
  
      return [cleanEndingSeparator(acc), PATH_SEPARATOR, cleanLeadingSeparator(path)].join('');
    }, '');
  }
  
  // Returns an array containing the value(s) of the
  // of the key passed as an argument
  export function getParameterValues(sParam, url = window.location) {
    const sPageURL = decodeURIComponent(new URL(url).search.substring(1));
  
    return sPageURL.split('&').reduce((acc, urlParam) => {
      const sParameterName = urlParam.split('=');
  
      if (sParameterName[0] === sParam) {
        acc.push(sParameterName[1].replace(/\+/g, ' '));
      }
  
      return acc;
    }, []);
  }
  
  /**
   * Merges a URL to a set of params replacing value for
   * those already present.
   *
   * Also removes `null` param values from the resulting URL.
   *
   * @param {Object} params - url keys and value to merge
   * @param {String} url
   * @param {Object} options
   * @param {Boolean} options.spreadArrays - split array values into separate key/value-pairs
   * @param {Boolean} options.sort - alphabetically sort params in the returned url (in asc order, i.e., a-z)
   */
  export function mergeUrlParams(params, url, options = {}) {
    const { spreadArrays = false, sort = false } = options;
    const re = /^([^?#]*)(\?[^#]*)?(.*)/;
    let merged = {};
    const [, fullpath, query, fragment] = url.match(re);
  
    if (query) {
      merged = query
        .substr(1)
        .split('&')
        .reduce((memo, part) => {
          if (part.length) {
            const kv = part.split('=');
            let key = decodeUrlParameter(kv[0]);
            const value = decodeUrlParameter(kv.slice(1).join('='));
            if (spreadArrays && key.endsWith('[]')) {
              key = key.slice(0, -2);
              if (!Array.isArray(memo[key])) {
                return { ...memo, [key]: [value] };
              }
              memo[key].push(value);
  
              return memo;
            }
  
            return { ...memo, [key]: value };
          }
  
          return memo;
        }, {});
    }
  
    Object.assign(merged, params);
  
    const mergedKeys = sort ? Object.keys(merged).sort() : Object.keys(merged);
  
    const newQuery = mergedKeys
      .filter((key) => merged[key] !== null)
      .map((key) => {
        let value = merged[key];
        const encodedKey = encodeURIComponent(key);
        if (spreadArrays && Array.isArray(value)) {
          value = merged[key]
            .map((arrayValue) => encodeURIComponent(arrayValue))
            .join(`&${encodedKey}[]=`);
          return `${encodedKey}[]=${value}`;
        }
        return `${encodedKey}=${encodeURIComponent(value)}`;
      })
      .join('&');
  
    if (newQuery) {
      return `${fullpath}?${newQuery}${fragment}`;
    }
    return `${fullpath}${fragment}`;
  }
  
  /**
   * Removes specified query params from the url by returning a new url string that no longer
   * includes the param/value pair. If no url is provided, `window.location.href` is used as
   * the default value.
   *
   * @param {string[]} params - the query param names to remove
   * @param {string} [url=windowLocation().href] - url from which the query param will be removed
   * @param {boolean} skipEncoding - set to true when the url does not require encoding
   * @returns {string} A copy of the original url but without the query param
   */
  export function removeParams(params, url = window.location.href, skipEncoding = false) {
    const [rootAndQuery, fragment] = url.split('#');
    const [root, query] = rootAndQuery.split('?');
  
    if (!query) {
      return url;
    }
  
    const removableParams = skipEncoding ? params : params.map((param) => encodeURIComponent(param));
  
    const updatedQuery = query
      .split('&')
      .filter((paramPair) => {
        const [foundParam] = paramPair.split('=');
        return removableParams.indexOf(foundParam) < 0;
      })
      .join('&');
  
    const writableQuery = updatedQuery.length > 0 ? `?${updatedQuery}` : '';
    const writableFragment = fragment ? `#${fragment}` : '';
    return `${root}${writableQuery}${writableFragment}`;
  }
  
  export const getLocationHash = (hash = window.location.hash) => hash.split('#')[1];
  
  /**
   * Returns a boolean indicating whether the URL hash contains the given string value
   */
  export function doesHashExistInUrl(hashName) {
    const hash = getLocationHash();
    return hash && hash.includes(hashName);
  }
  
  export function urlContainsSha({ url = String(window.location) } = {}) {
    return resetRegExp(SHA_REGEX).test(url);
  }
  
  export function getShaFromUrl({ url = String(window.location) } = {}) {
    let sha = null;
  
    if (urlContainsSha({ url })) {
      [sha] = url.match(resetRegExp(SHA_REGEX));
    }
  
    return sha;
  }
  
  /**
   * Apply the fragment to the given url by returning a new url string that includes
   * the fragment. If the given url already contains a fragment, the original fragment
   * will be removed.
   *
   * @param {string} url - url to which the fragment will be applied
   * @param {string} fragment - fragment to append
   */
  export const setUrlFragment = (url, fragment) => {
    const [rootUrl] = url.split('#');
    const encodedFragment = encodeURIComponent(fragment.replace(/^#/, ''));
    return `${rootUrl}#${encodedFragment}`;
  };
  
  export function visitUrl(url, external = false) {
    if (external) {
      // Simulate `target="_blank" rel="noopener noreferrer"`
      // See https://mathiasbynens.github.io/rel-noopener/
      const otherWindow = window.open();
      otherWindow.opener = null;
      otherWindow.location = url;
    } else {
      window.location.href = url;
    }
  }
  
  export function updateHistory({ state = {}, title = '', url, replace = false, win = window } = {}) {
    if (win.history) {
      if (replace) {
        win.history.replaceState(state, title, url);
      } else {
        win.history.pushState(state, title, url);
      }
    }
  }
  
  export function refreshCurrentPage() {
    visitUrl(window.location.href);
  }
  
  export function redirectTo(url) {
    return window.location.assign(url);
  }
  
  export const escapeFileUrl = (fileUrl) => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
  
  export function webIDEUrl(route = undefined) {
    let returnUrl = `${gon.relative_url_root || ''}/-/ide/`;
    if (route) {
      returnUrl += `project${route.replace(new RegExp(`^${gon.relative_url_root || ''}`), '')}`;
    }
    return escapeFileUrl(returnUrl);
  }
  
  /**
   * Returns current base URL
   */
  export function getBaseURL() {
    const { protocol, host } = window.location;
    return `${protocol}//${host}`;
  }
  
  /**
   * Takes a URL and returns content from the start until the final '/'
   *
   * @param {String} url - full url, including protocol and host
   */
  export function stripFinalUrlSegment(url) {
    return new URL('.', url).href;
  }
  
  /**
   * Returns true if url is an absolute URL
   *
   * @param {String} url
   */
  export function isAbsolute(url) {
    return /^https?:\/\//.test(url);
  }
  
  /**
   * Returns true if url is a root-relative URL
   *
   * @param {String} url
   */
  export function isRootRelative(url) {
    return /^\/(?!\/)/.test(url);
  }
  
  /**
   * Returns true if url is a base64 data URL
   *
   * @param {String} url
   */
  export function isBase64DataUrl(url) {
    return /^data:[.\w+-]+\/[.\w+-]+;base64,/.test(url);
  }
  
  /**
   * Returns true if url is a blob: type url
   *
   * @param {String} url
   */
  export function isBlobUrl(url) {
    return /^blob:/.test(url);
  }
  
  /**
   * Returns true if url is an absolute or root-relative URL
   *
   * @param {String} url
   */
  export function isAbsoluteOrRootRelative(url) {
    return isAbsolute(url) || isRootRelative(url);
  }
  
  /**
   * Returns true if url is an external URL
   *
   * @param {String} url
   * @returns {Boolean}
   */
  export function isExternal(url) {
    if (isRootRelative(url)) {
      return false;
    }
  
    return !url.includes(gon.gitlab_url);
  }
  
  /**
   * Converts a relative path to an absolute or a root relative path depending
   * on what is passed as a basePath.
   *
   * @param {String} path       Relative path, eg. ../img/img.png
   * @param {String} basePath   Absolute or root relative path, eg. /user/project or
   *                            https://gitlab.com/user/project
   */
  export function relativePathToAbsolute(path, basePath) {
    const absolute = isAbsolute(basePath);
    const base = absolute ? basePath : `file:///${basePath}`;
    const url = new URL(path, base);
    return absolute ? url.href : decodeURIComponent(url.pathname);
  }
  
  /**
   * Checks if the provided URL is a safe URL (absolute http(s) or root-relative URL)
   *
   * @param {String} url that will be checked
   * @returns {Boolean}
   */
  export function isSafeURL(url) {
    if (!isAbsoluteOrRootRelative(url)) {
      return false;
    }
  
    try {
      const parsedUrl = new URL(url, getBaseURL());
      return ['http:', 'https:'].includes(parsedUrl.protocol);
    } catch (e) {
      return false;
    }
  }
  
  /**
   * Returns a normalized url
   *
   * https://gitlab.com/foo/../baz => https://gitlab.com/baz
   *
   * @param {String} url - URL to be transformed
   * @param {String?} baseUrl - current base URL
   * @returns {String}
   */
  export const getNormalizedURL = (url, baseUrl) => {
    const base = baseUrl || getBaseURL();
    try {
      return new URL(url, base).href;
    } catch (e) {
      return '';
    }
  };
  
  export function getWebSocketProtocol() {
    return window.location.protocol.replace('http', 'ws');
  }
  
  export function getWebSocketUrl(path) {
    return `${getWebSocketProtocol()}//${joinPaths(window.location.host, path)}`;
  }
  
  const splitPath = (path = '') => path.replace(/^\?/, '').split('&');
  
  export const urlParamsToArray = (path = '') =>
    splitPath(path)
      .filter((param) => param.length > 0)
      .map((param) => {
        const split = param.split('=');
        return [decodeURI(split[0]), split[1]].join('=');
      });
  
  export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
  
  /**
   * Convert search query into an object
   *
   * @param {String} query from "document.location.search"
   * @param {Object} options
   * @param {Boolean?} options.gatherArrays - gather array values into an Array
   * @param {Boolean?} options.legacySpacesDecode - (deprecated) plus symbols (+) are not replaced with spaces, false by default
   * @returns {Object}
   *
   * ex: "?one=1&two=2" into {one: 1, two: 2}
   */
  export function queryToObject(query, { gatherArrays = false, legacySpacesDecode = false } = {}) {
    const removeQuestionMarkFromQuery = String(query).startsWith('?') ? query.slice(1) : query;
    return removeQuestionMarkFromQuery.split('&').reduce((accumulator, curr) => {
      const [key, value] = curr.split('=');
      if (value === undefined) {
        return accumulator;
      }
  
      const decodedValue = legacySpacesDecode ? decodeURIComponent(value) : decodeUrlParameter(value);
      const decodedKey = legacySpacesDecode ? decodeURIComponent(key) : decodeUrlParameter(key);
  
      if (gatherArrays && decodedKey.endsWith('[]')) {
        const decodedArrayKey = decodedKey.slice(0, -2);
  
        if (!Array.isArray(accumulator[decodedArrayKey])) {
          accumulator[decodedArrayKey] = [];
        }
  
        accumulator[decodedArrayKey].push(decodedValue);
      } else {
        accumulator[decodedKey] = decodedValue;
      }
  
      return accumulator;
    }, {});
  }
  
  /**
   * This function accepts the `name` of the param to parse in the url
   * if the name does not exist this function will return `null`
   * otherwise it will return the value of the param key provided
   *
   * @param {String} name
   * @param {String?} urlToParse
   * @returns value of the parameter as string
   */
  export const getParameterByName = (name, query = window.location.search) => {
    return queryToObject(query)[name] || null;
  };
  
  /**
   * Convert search query object back into a search query
   *
   * @param {Object?} params that needs to be converted
   * @returns {String}
   *
   * ex: {one: 1, two: 2} into "one=1&two=2"
   *
   */
  export function objectToQuery(params = {}) {
    return Object.keys(params)
      .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
      .join('&');
  }
  
  /**
   * Sets query params for a given URL
   * It adds new query params, updates existing params with a new value and removes params with value null/undefined
   *
   * @param {Object} params The query params to be set/updated
   * @param {String} url The url to be operated on
   * @param {Boolean} clearParams Indicates whether existing query params should be removed or not
   * @param {Boolean} railsArraySyntax When enabled, changes the array syntax from `keys=` to `keys[]=` according to Rails conventions
   * @returns {String} A copy of the original with the updated query params
   */
  export const setUrlParams = (
    params,
    url = window.location.href,
    clearParams = false,
    railsArraySyntax = false,
    decodeParams = false,
  ) => {
    const urlObj = new URL(url);
    const queryString = urlObj.search;
    const searchParams = clearParams ? new URLSearchParams('') : new URLSearchParams(queryString);
  
    Object.keys(params).forEach((key) => {
      if (params[key] === null || params[key] === undefined) {
        searchParams.delete(key);
      } else if (Array.isArray(params[key])) {
        const keyName = railsArraySyntax ? `${key}[]` : key;
        if (params[key].length === 0) {
          searchParams.delete(keyName);
        } else {
          params[key].forEach((val, idx) => {
            if (idx === 0) {
              searchParams.set(keyName, val);
            } else {
              searchParams.append(keyName, val);
            }
          });
        }
      } else {
        searchParams.set(key, params[key]);
      }
    });
  
    urlObj.search = decodeParams
      ? decodeURIComponent(searchParams.toString())
      : searchParams.toString();
  
    return urlObj.toString();
  };
  
  export function urlIsDifferent(url, compare = String(window.location)) {
    return url !== compare;
  }
  
  export function getHTTPProtocol(url) {
    if (!url) {
      return window.location.protocol.slice(0, -1);
    }
    const protocol = url.split(':');
    return protocol.length > 1 ? protocol[0] : undefined;
  }
  
  /**
   * Strips the filename from the given path by removing every non-slash character from the end of the
   * passed parameter.
   * @param {string} path
   */
  export function stripPathTail(path = '') {
    return path.replace(/[^/]+$/, '');
  }
  
  export function getURLOrigin(url) {
    if (!url) {
      return window.location.origin;
    }
  
    try {
      return new URL(url).origin;
    } catch (e) {
      return null;
    }
  }
  
  /**
   * Returns `true` if the given `url` resolves to the same origin the page is served
   * from; otherwise, returns `false`.
   *
   * The `url` may be absolute or relative.
   *
   * @param {string} url The URL to check.
   * @returns {boolean}
   */
  export function isSameOriginUrl(url) {
    if (typeof url !== 'string') {
      return false;
    }
  
    const { origin } = window.location;
  
    try {
      return new URL(url, origin).origin === origin;
    } catch {
      // Invalid URLs cannot have the same origin
      return false;
    }
  }
  
  /**
   * Returns a URL to WebIDE considering the current user's position in
   * repository's tree. If not MR `iid` has been passed, the URL is fetched
   * from the global `gl.webIDEPath`.
   *
   * @param sourceProjectFullPath Source project's full path. Used in MRs
   * @param targetProjectFullPath Target project's full path. Used in MRs
   * @param iid                   MR iid
   * @returns {string}
   */
  
  export function constructWebIDEPath({
    sourceProjectFullPath,
    targetProjectFullPath = '',
    iid,
  } = {}) {
    if (!iid || !sourceProjectFullPath) {
      return window.gl?.webIDEPath;
    }
    return mergeUrlParams(
      {
        target_project: sourceProjectFullPath !== targetProjectFullPath ? targetProjectFullPath : '',
      },
      webIDEUrl(`/${sourceProjectFullPath}/merge_requests/${iid}`),
    );
  }
  