All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.core.location.location.js Maven / Gradle / Ivy

import { JQLite } from "../../shared/jqlite/jqlite";
import { urlResolve } from "../url-utils/url-utils";
import {
  encodeUriSegment,
  isBoolean,
  isDefined,
  isNumber,
  isObject,
  isString,
  isUndefined,
  minErr,
  parseKeyValue,
  toInt,
  toKeyValue,
} from "../../shared/utils";

/**
 * @typedef {Object} DefaultPorts
 * @property {number} http
 * @property {number} https
 * @property {number} ftp
 */

/**
 * @typedef {Object} Html5Mode
 * @property {boolean} enabled
 * @property {boolean} requireBase
 * @property {boolean|string} rewriteLinks
 */

/** @type {DefaultPorts} */
const DEFAULT_PORTS = { http: 80, https: 443, ftp: 21 };
const PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/;
const $locationMinErr = minErr("$location");

/**
 * @abstract
 */
export class Location {
  /**
   * @param {string} appBase application base URL
   * @param {string} appBaseNoFile application base URL stripped of any filename
   */
  constructor(appBase, appBaseNoFile) {
    /** @type {string} */
    this.appBase = appBase;

    /** @type {string} */
    this.appBaseNoFile = appBaseNoFile;

    /**
     * An absolute URL is the full URL, including protocol (http/https ), the optional subdomain (e.g. www ), domain (example.com), and path (which includes the directory and slug).
     * @type {string}
     */
    this.$$absUrl = "";

    /**
     * If html5 mode is enabled
     * @type {boolean}
     */
    this.$$html5 = false;

    /**
     * Has any change been replacing?
     * @type {boolean}
     */
    this.$$replace = false;

    /** @type {import('../url-utils/url-utils').HttpProtocol} */
    this.$$protocol = undefined;

    /** @type {string} */
    this.$$host = undefined;

    /**
     * The port, without ":"
     * @type {number}
     */
    this.$$port = undefined;

    /**
     * The pathname, beginning with "/"
     * @type {string}
     */
    this.$$path = undefined;

    /**
     * The hash string, minus the hash symbol
     * @type {string}
     */
    this.$$hash = undefined;

    /**
     * Helper property for scope watch changes
     * @type {boolean}
     */
    this.$$urlUpdatedByLocation = false;
  }

  /**
   * Return full URL representation with all segments encoded according to rules specified in
   * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt).
   *
   * @return {string} full URL
   */
  absUrl() {
    return this.$$absUrl;
  }

  /**
   * This method is getter / setter.
   *
   * Return URL (e.g. `/path?a=b#hash`) when called without any parameter.
   * Change path, search and hash, when called with parameter and return `$location`.
   *
   * @param {string=} url New URL without base prefix (e.g. `/path?a=b#hash`)
   * @return {Location|string} url
   */
  url(url) {
    if (isUndefined(url)) {
      return this.$$url;
    }

    const match = PATH_MATCH.exec(url);
    if (match[1] || url === "") this.path(decodeURIComponent(match[1]));
    if (match[2] || match[1] || url === "") this.search(match[3] || "");
    this.hash(match[5] || "");

    return this;
  }

  /**
   *
   * Return protocol of current URL.
   * @return {import("../url-utils/url-utils").HttpProtocol} protocol of current URL
   */
  protocol() {
    return this.$$protocol;
  }

  /**
   * This method is getter only.
   *
   * Return host of current URL.
   *
   * Note: compared to the non-AngularJS version `location.host` which returns `hostname:port`, this returns the `hostname` portion only.
   *
   *
   * @return {string} host of current URL.
   */
  host() {
    return this.$$host;
  }

  /**
   * This method is getter only.
   *
   * Return port of current URL.
   *
   *
   * ```js
   * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
   * let port = $location.port();
   * // => 80
   * ```
   *
   * @return {number} port
   */
  port() {
    return this.$$port;
  }

  /**
   * This method is getter / setter.
   *
   * Return path of current URL when called without any parameter.
   *
   * Change path when called with parameter and return `$location`.
   *
   * Note: Path should always begin with forward slash (/), this method will add the forward slash
   * if it is missing.
   *
   *
   * ```js
   * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
   * let path = $location.path();
   * // => "/some/path"
   * ```
   *
   * @param {(string|number)=} path New path
   * @return {(string|object)} path if called with no parameters, or `$location` if called with a parameter
   */
  path(path) {
    if (isUndefined(path)) {
      return this.$$path;
    }
    let newPath = path !== null ? path.toString() : "";
    this.$$path = newPath.charAt(0) === "/" ? newPath : `/${newPath}`;
    this.$$compose();
    return this;
  }

  /**
   * This method is getter / setter.
   *
   * Returns the hash fragment when called without any parameters.
   *
   * Changes the hash fragment when called with a parameter and returns `$location`.
   *
   *
   * ```js
   * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue
   * let hash = $location.hash();
   * // => "hashValue"
   * ```
   *
   * @param {(string|number)=} hash New hash fragment
   * @return {string|Location} hash
   */
  hash(hash) {
    if (isUndefined(hash)) {
      return this.$$hash;
    }

    this.$$hash = hash !== null ? hash.toString() : "";
    this.$$compose();
    return this;
  }

  /**
   * If called, all changes to $location during the current `$digest` will replace the current history
   * record, instead of adding a new one.
   */
  replace() {
    this.$$replace = true;
    return this;
  }

  /**
   * Returns or sets the search part (as object) of current URL when called without any parameter
   *
   * @param {string|Object=} search New search params - string or hash object.
   * @param {(string|number|Array|boolean)=} paramValue If search is a string or number, then paramValue will override only a single search property.
   * @returns {Object|Location} Search object or Location object
   */
  search(search, paramValue) {
    switch (arguments.length) {
      case 0:
        return this.$$search;
      case 1:
        if (isString(search) || isNumber(search)) {
          search = search.toString();
          this.$$search = parseKeyValue(search);
        } else if (isObject(search)) {
          search = structuredClone(search, {});
          // remove object undefined or null properties
          Object.entries(search).forEach(([key, value]) => {
            if (value == null) delete search[key];
          });

          this.$$search = search;
        } else {
          throw $locationMinErr(
            "isrcharg",
            "The first argument of the `$location#search()` call must be a string or an object.",
          );
        }
        break;
      default:
        if (isUndefined(paramValue) || paramValue === null) {
          delete this.$$search[search];
        } else {
          this.$$search[search] = paramValue;
        }
    }

    this.$$compose();
    return this;
  }

  /**
   * Compose url and update `url` and `absUrl` property
   * @returns {void}
   */
  $$compose() {
    this.$$url = normalizePath(this.$$path, this.$$search, this.$$hash);
    this.$$absUrl = this.$$normalizeUrl(this.$$url);
    this.$$urlUpdatedByLocation = true;
  }

  /**
   * @param {string} _url
   * @returns {string}
   */
  $$normalizeUrl(_url) {
    throw new Error(`Method not implemented ${_url}`);
  }

  /**
   * This method is getter / setter.
   *
   * Return the history state object when called without any parameter.
   *
   * Change the history state object when called with one parameter and return `$location`.
   * The state object is later passed to `pushState` or `replaceState`.
   * See {@link https://developer.mozilla.org/en-US/docs/Web/API/History/pushState#state|History.state}
   *
   * NOTE: This method is supported only in HTML5 mode and only in browsers supporting
   * the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support
   * older browsers (like IE9 or Android < 4.0), don't use this method.
   *
   * @param {any} state State object for pushState or replaceState
   * @return {any} state
   */
  state(state) {
    if (!arguments.length) {
      return this.$$state;
    }

    if (!(this instanceof LocationHtml5Url) || !this.$$html5) {
      throw $locationMinErr(
        "nostate",
        "History API state support is available only " +
          "in HTML5 mode and only in browsers supporting HTML5 History API",
      );
    }
    // The user might modify `stateObject` after invoking `$location.state(stateObject)`
    // but we're changing the $$state reference to $browser.state() during the $digest
    // so the modification window is narrow.
    this.$$state = isUndefined(state) ? null : state;
    this.$$urlUpdatedByLocation = true;

    return this;
  }

  /**
   * @param {string} _url
   * @param {string} _url2
   * @returns {boolean}
   */
  $$parseLinkUrl(_url, _url2) {
    throw new Error(`Method not implemented ${_url} ${_url2}`);
  }

  $$parse(_url) {
    throw new Error(`Method not implemented ${_url}`);
  }
}

/**
 * This object is exposed as $location service when HTML5 mode is enabled and supported
 */
export class LocationHtml5Url extends Location {
  /**
   * @param {string} appBase application base URL
   * @param {string} appBaseNoFile application base URL stripped of any filename
   * @param {string} basePrefix URL path prefix
   */
  constructor(appBase, appBaseNoFile, basePrefix) {
    super(appBase, appBaseNoFile);
    this.$$html5 = true;
    this.basePrefix = basePrefix || "";
    parseAbsoluteUrl(appBase, this);
  }

  /**
   * Parse given HTML5 (regular) URL string into properties
   * @param {string} url HTML5 URL
   */
  $$parse(url) {
    const pathUrl = stripBaseUrl(this.appBaseNoFile, url);
    if (!isString(pathUrl)) {
      throw $locationMinErr(
        "ipthprfx",
        'Invalid url "{0}", missing path prefix "{1}".',
        url,
        this.appBaseNoFile,
      );
    }

    parseAppUrl(pathUrl, this, true);

    if (!this.$$path) {
      this.$$path = "/";
    }

    this.$$compose();
  }

  $$normalizeUrl(url) {
    return this.appBaseNoFile + url.substr(1); // first char is always '/'
  }

  /**
   * @param {string} url
   * @param {string} relHref
   * @returns {boolean}
   */
  $$parseLinkUrl(url, relHref) {
    if (relHref && relHref[0] === "#") {
      // special case for links to hash fragments:
      // keep the old url and only replace the hash fragment
      this.hash(relHref.slice(1));
      return true;
    }
    let appUrl;
    let prevAppUrl;
    let rewrittenUrl;

    if (isDefined((appUrl = stripBaseUrl(this.appBase, url)))) {
      prevAppUrl = appUrl;
      if (
        this.basePrefix &&
        isDefined((appUrl = stripBaseUrl(this.basePrefix, appUrl)))
      ) {
        rewrittenUrl =
          this.appBaseNoFile + (stripBaseUrl("/", appUrl) || appUrl);
      } else {
        rewrittenUrl = this.appBase + prevAppUrl;
      }
    } else if (isDefined((appUrl = stripBaseUrl(this.appBaseNoFile, url)))) {
      rewrittenUrl = this.appBaseNoFile + appUrl;
    } else if (this.appBaseNoFile === `${url}/`) {
      rewrittenUrl = this.appBaseNoFile;
    }
    if (rewrittenUrl) {
      this.$$parse(rewrittenUrl);
    }
    return !!rewrittenUrl;
  }
}

/**
 * LocationHashbangUrl represents URL
 * This object is exposed as $location service when developer doesn't opt into html5 mode.
 * It also serves as the base class for html5 mode fallback on legacy browsers.
 *
 * @constructor
 * @param {string} appBase application base URL
 * @param {string} appBaseNoFile application base URL stripped of any filename
 * @param {string} hashPrefix hashbang prefix
 */
export class LocationHashbangUrl extends Location {
  constructor(appBase, appBaseNoFile, hashPrefix) {
    super(appBase, appBaseNoFile);
    this.appBase = appBase;
    this.appBaseNoFile = appBaseNoFile;
    this.hashPrefix = hashPrefix;
    parseAbsoluteUrl(appBase, this);
  }

  /**
   * Parse given hashbang URL into properties
   * @param {string} url Hashbang URL
   */
  $$parse(url) {
    const withoutBaseUrl =
      stripBaseUrl(this.appBase, url) || stripBaseUrl(this.appBaseNoFile, url);
    let withoutHashUrl;

    if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === "#") {
      // The rest of the URL starts with a hash so we have
      // got either a hashbang path or a plain hash fragment
      withoutHashUrl = stripBaseUrl(this.hashPrefix, withoutBaseUrl);
      if (isUndefined(withoutHashUrl)) {
        // There was no hashbang prefix so we just have a hash fragment
        withoutHashUrl = withoutBaseUrl;
      }
    } else {
      // There was no hashbang path nor hash fragment:
      // If we are in HTML5 mode we use what is left as the path;
      // Otherwise we ignore what is left
      if (this.$$html5) {
        withoutHashUrl = withoutBaseUrl;
      } else {
        withoutHashUrl = "";
        if (isUndefined(withoutBaseUrl)) {
          this.appBase = url;
          /** @type {?} */ (this).replace();
        }
      }
    }

    parseAppUrl(withoutHashUrl, this, false);

    this.$$path = removeWindowsDriveName(
      this.$$path,
      withoutHashUrl,
      this.appBase,
    );

    this.$$compose();

    /*
     * In Windows, on an anchor node on documents loaded from
     * the filesystem, the browser will return a pathname
     * prefixed with the drive name ('/C:/path') when a
     * pathname without a drive is set:
     *  * a.setAttribute('href', '/foo')
     *   * a.pathname === '/C:/foo' //true
     *
     * Inside of AngularJS, we're always using pathnames that
     * do not include drive names for routing.
     */
    function removeWindowsDriveName(path, url, base) {
      /*
      Matches paths for file protocol on windows,
      such as /C:/foo/bar, and captures only /foo/bar.
      */
      const windowsFilePathExp = /^\/[A-Z]:(\/.*)/;

      let firstPathSegmentMatch;

      // Get the relative path from the input URL.
      if (startsWith(url, base)) {
        url = url.replace(base, "");
      }

      // The input URL intentionally contains a first path segment that ends with a colon.
      if (windowsFilePathExp.exec(url)) {
        return path;
      }

      firstPathSegmentMatch = windowsFilePathExp.exec(path);
      return firstPathSegmentMatch ? firstPathSegmentMatch[1] : path;
    }
  }

  $$normalizeUrl(url) {
    return this.appBase + (url ? this.hashPrefix + url : "");
  }

  /**
   * @param {string} url
   * @returns {boolean}
   */
  $$parseLinkUrl(url) {
    if (stripHash(this.appBase) === stripHash(url)) {
      this.$$parse(url);
      return true;
    }
    return false;
  }
}

export function LocationProvider() {
  let hashPrefix = "!";
  const html5Mode = {
    enabled: false,
    requireBase: true,
    rewriteLinks: true,
  };

  /**
   * The default value for the prefix is `'!'`.
   * @param {string=} prefix Prefix for hash part (containing path and search)
   * @returns {*} current value if used as getter or itself (chaining) if used as setter
   */
  this.hashPrefix = function (prefix) {
    if (isDefined(prefix)) {
      hashPrefix = prefix;
      return this;
    }
    return hashPrefix;
  };

  /**
   * @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value.
   *   If object, sets `enabled`, `requireBase` and `rewriteLinks` to respective values. Supported
   *   properties:
   *   - **enabled** – `{boolean}` – (default: false) If true, will rely on `history.pushState` to
   *     change urls where supported. Will fall back to hash-prefixed paths in browsers that do not
   *     support `pushState`.
   *   - **requireBase** - `{boolean}` - (default: `true`) When html5Mode is enabled, specifies
   *     whether or not a  tag is required to be present. If `enabled` and `requireBase` are
   *     true, and a base tag is not present, an error will be thrown when `$location` is injected.
   *     See the {@link guide/$location $location guide for more information}
   *   - **rewriteLinks** - `{boolean|string}` - (default: `true`) When html5Mode is enabled,
   *     enables/disables URL rewriting for relative links. If set to a string, URL rewriting will
   *     only happen on links with an attribute that matches the given string. For example, if set
   *     to `'internal-link'`, then the URL will only be rewritten for `` links.
   *     Note that [attribute name normalization](guide/directive#normalization) does not apply
   *     here, so `'internalLink'` will **not** match `'internal-link'`.
   *
   * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter
   */
  this.html5Mode = function (mode) {
    if (isBoolean(mode)) {
      html5Mode.enabled = mode;
      return this;
    }
    if (isObject(mode)) {
      if (isBoolean(mode.enabled)) {
        html5Mode.enabled = mode.enabled;
      }

      if (isBoolean(mode.requireBase)) {
        html5Mode.requireBase = mode.requireBase;
      }

      if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) {
        html5Mode.rewriteLinks = mode.rewriteLinks;
      }

      return this;
    }
    return html5Mode;
  };

  this.$get = [
    "$rootScope",
    "$browser",
    "$rootElement",
    /**
     *
     * @param {import('../scope/scope').Scope} $rootScope
     * @param {import('../../services/browser').Browser} $browser
     * @param {JQLite} $rootElement
     * @returns
     */
    function ($rootScope, $browser, $rootElement) {
      /** @type {Location} */
      let $location;
      let LocationMode;
      const baseHref = $browser.baseHref(); // if base[href] is undefined, it defaults to ''
      const initialUrl = /** @type {string} */ ($browser.url());
      let appBase;

      if (html5Mode.enabled) {
        if (!baseHref && html5Mode.requireBase) {
          throw $locationMinErr(
            "nobase",
            "$location in HTML5 mode requires a  tag to be present!",
          );
        }
        appBase = serverBase(initialUrl) + (baseHref || "/");
        LocationMode = LocationHtml5Url;
      } else {
        appBase = stripHash(initialUrl);
        LocationMode = LocationHashbangUrl;
      }
      const appBaseNoFile = stripFile(appBase);

      $location = new LocationMode(appBase, appBaseNoFile, `#${hashPrefix}`);
      $location.$$parseLinkUrl(initialUrl, initialUrl);

      $location.$$state = $browser.state();

      const IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;

      function setBrowserUrlWithFallback(url, state) {
        const oldUrl = $location.url();
        const oldState = $location.$$state;
        try {
          $browser.url(url, state);

          // Make sure $location.state() returns referentially identical (not just deeply equal)
          // state object; this makes possible quick checking if the state changed in the digest
          // loop. Checking deep equality would be too expensive.
          $location.$$state = $browser.state();
        } catch (e) {
          // Restore old values if pushState fails
          $location.url(/** @type {string} */ (oldUrl));
          $location.$$state = oldState;

          throw e;
        }
      }

      $rootElement.on("click", (event) => {
        const { rewriteLinks } = html5Mode;
        // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
        // currently we open nice url link and redirect then

        if (
          !rewriteLinks ||
          event.ctrlKey ||
          event.metaKey ||
          event.shiftKey ||
          event.which === 2 ||
          event.button === 2
        )
          return;

        let elm = JQLite(event.target);

        // traverse the DOM up to find first A tag
        while (elm[0].nodeName.toLowerCase() !== "a") {
          // ignore rewriting if no A tag (reached root element, or no parent - removed from document)
          if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return;
        }

        if (isString(rewriteLinks) && isUndefined(elm.attr(rewriteLinks)))
          return;

        let absHref = elm[0].href;
        // get the actual href attribute - see
        // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx
        const relHref = elm.attr("href") || elm.attr("xlink:href");

        if (
          isObject(absHref) &&
          absHref.toString() === "[object SVGAnimatedString]"
        ) {
          // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during
          // an animation.
          absHref = urlResolve(absHref.animVal).href;
        }

        // Ignore when url is started with javascript: or mailto:
        if (IGNORE_URI_REGEXP.test(absHref)) return;

        if (absHref && !elm.attr("target") && !event.isDefaultPrevented()) {
          if ($location.$$parseLinkUrl(absHref, relHref)) {
            // We do a preventDefault for all urls that are part of the AngularJS application,
            // in html5mode and also without, so that we are able to abort navigation without
            // getting double entries in the location history.
            event.preventDefault();
            // update location manually
            if ($location.absUrl() !== $browser.url()) {
              $rootScope.$apply();
            }
          }
        }
      });

      // rewrite hashbang url <> html5 url
      if ($location.absUrl() !== initialUrl) {
        $browser.url($location.absUrl(), true);
      }

      let initializing = true;

      // update $location when $browser url changes
      $browser.onUrlChange((newUrl, newState) => {
        if (!startsWith(newUrl, appBaseNoFile)) {
          // If we are navigating outside of the app then force a reload
          window.location.href = newUrl;
          return;
        }

        $rootScope.$evalAsync(() => {
          const oldUrl = $location.absUrl();
          const oldState = $location.$$state;
          let defaultPrevented;
          $location.$$parse(newUrl);
          $location.$$state = newState;

          defaultPrevented = $rootScope.$broadcast(
            "$locationChangeStart",
            newUrl,
            oldUrl,
            newState,
            oldState,
          ).defaultPrevented;

          // if the location was changed by a `$locationChangeStart` handler then stop
          // processing this location change
          if ($location.absUrl() !== newUrl) return;

          if (defaultPrevented) {
            $location.$$parse(oldUrl);
            $location.$$state = oldState;
            setBrowserUrlWithFallback(oldUrl, oldState);
          } else {
            initializing = false;
            afterLocationChange(oldUrl, oldState);
          }
        });
      });

      // update browser
      $rootScope.$watch(() => {
        if (initializing || $location.$$urlUpdatedByLocation) {
          $location.$$urlUpdatedByLocation = false;

          const oldUrl = /** @type {string} */ ($browser.url());
          const newUrl = $location.absUrl();
          const oldState = $browser.state();
          const urlOrStateChanged =
            !urlsEqual(oldUrl, newUrl) ||
            ($location.$$html5 && oldState !== $location.$$state);

          if (initializing || urlOrStateChanged) {
            initializing = false;

            $rootScope.$evalAsync(() => {
              const newUrl = $location.absUrl();
              const { defaultPrevented } = $rootScope.$broadcast(
                "$locationChangeStart",
                newUrl,
                oldUrl,
                $location.$$state,
                oldState,
              );

              // if the location was changed by a `$locationChangeStart` handler then stop
              // processing this location change
              if ($location.absUrl() !== newUrl) return;

              if (defaultPrevented) {
                $location.$$parse(oldUrl);
                $location.$$state = oldState;
              } else {
                if (urlOrStateChanged) {
                  setBrowserUrlWithFallback(
                    newUrl,
                    oldState === $location.$$state ? null : $location.$$state,
                  );
                }
                afterLocationChange(oldUrl, oldState);
              }
            });
          }
        }

        $location.$$replace = false;

        // we don't need to return anything because $evalAsync will make the digest loop dirty when
        // there is a change
      });

      return $location;

      function afterLocationChange(oldUrl, oldState) {
        $rootScope.$broadcast(
          "$locationChangeSuccess",
          $location.absUrl(),
          oldUrl,
          $location.$$state,
          oldState,
        );
      }
    },
  ];
}

/**
 * ///////////////////////////
 *          HELPERS
 * ///////////////////////////
 */

/**
 * Encode path using encodeUriSegment, ignoring forward slashes
 *
 * @param {string} path Path to encode
 * @returns {string}
 */
function encodePath(path) {
  const segments = path.split("/");
  let i = segments.length;

  while (i--) {
    // decode forward slashes to prevent them from being double encoded
    segments[i] = encodeUriSegment(segments[i].replace(/%2F/g, "/"));
  }

  return segments.join("/");
}

function decodePath(path, html5Mode) {
  const segments = path.split("/");
  let i = segments.length;

  while (i--) {
    segments[i] = decodeURIComponent(segments[i]);
    if (html5Mode) {
      // encode forward slashes to prevent them from being mistaken for path separators
      segments[i] = segments[i].replace(/\//g, "%2F");
    }
  }

  return segments.join("/");
}

function normalizePath(pathValue, searchValue, hashValue) {
  const search = toKeyValue(searchValue);
  const hash = hashValue ? `#${encodeUriSegment(hashValue)}` : "";
  const path = encodePath(pathValue);

  return path + (search ? `?${search}` : "") + hash;
}

/**
 * @param {string} absoluteUrl
 * @param {Location} locationObj
 */
function parseAbsoluteUrl(absoluteUrl, locationObj) {
  const parsedUrl = urlResolve(absoluteUrl);

  locationObj.$$protocol = parsedUrl.protocol;
  locationObj.$$host = parsedUrl.hostname;
  locationObj.$$port =
    toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
}

function parseAppUrl(url, locationObj, html5Mode) {
  if (/^\s*[\\/]{2,}/.test(url)) {
    throw $locationMinErr("badpath", 'Invalid url "{0}".', url);
  }

  const prefixed = url.charAt(0) !== "/";
  if (prefixed) {
    url = `/${url}`;
  }
  const match = urlResolve(url);
  const path =
    prefixed && match.pathname.charAt(0) === "/"
      ? match.pathname.substring(1)
      : match.pathname;
  locationObj.$$path = decodePath(path, html5Mode);
  locationObj.$$search = parseKeyValue(match.search);
  locationObj.$$hash = decodeURIComponent(match.hash);

  // make sure path starts with '/';
  if (locationObj.$$path && locationObj.$$path.charAt(0) !== "/") {
    locationObj.$$path = `/${locationObj.$$path}`;
  }
}

function startsWith(str, search) {
  return str.slice(0, search.length) === search;
}

/**
 *
 * @param {string} base
 * @param {string} url
 * @returns {string} returns text from `url` after `base` or `undefined` if it does not begin with
 *                   the expected string.
 */
export function stripBaseUrl(base, url) {
  if (startsWith(url, base)) {
    return url.substr(base.length);
  }
}

export function stripHash(url) {
  const index = url.indexOf("#");
  return index === -1 ? url : url.substr(0, index);
}

export function stripFile(url) {
  return url.substr(0, stripHash(url).lastIndexOf("/") + 1);
}

/* return the server only (scheme://host:port) */
export function serverBase(url) {
  return url.substring(0, url.indexOf("/", url.indexOf("//") + 2));
}

// Determine if two URLs are equal despite potentially having different encoding/normalizing
//  such as $location.absUrl() vs $browser.url()
// See https://github.com/angular/angular.js/issues/16592
function urlsEqual(a, b) {
  return a === b || urlResolve(a).href === urlResolve(b).href;
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy