2016-06-23 12:47:54 -04:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
2017-03-17 13:09:42 -04:00
|
|
|
import {PRIMARY_OUTLET, ParamMap, convertToParamMap} from './shared';
|
2016-06-14 17:55:59 -04:00
|
|
|
import {forEach, shallowEqual} from './utils/collection';
|
2016-05-26 19:50:59 -04:00
|
|
|
|
|
|
|
export function createEmptyUrlTree() {
|
2016-07-25 15:15:07 -04:00
|
|
|
return new UrlTree(new UrlSegmentGroup([], {}), {}, null);
|
2016-05-26 19:50:59 -04:00
|
|
|
}
|
2016-05-21 20:35:55 -04:00
|
|
|
|
2016-06-15 12:01:05 -04:00
|
|
|
export function containsTree(container: UrlTree, containee: UrlTree, exact: boolean): boolean {
|
|
|
|
if (exact) {
|
2016-11-11 16:23:47 -05:00
|
|
|
return equalQueryParams(container.queryParams, containee.queryParams) &&
|
|
|
|
equalSegmentGroups(container.root, containee.root);
|
2016-06-15 12:01:05 -04:00
|
|
|
}
|
2016-12-09 13:44:46 -05:00
|
|
|
|
|
|
|
return containsQueryParams(container.queryParams, containee.queryParams) &&
|
2016-12-09 14:19:55 -05:00
|
|
|
containsSegmentGroup(container.root, containee.root);
|
2016-06-15 12:01:05 -04:00
|
|
|
}
|
|
|
|
|
2016-11-11 16:23:47 -05:00
|
|
|
function equalQueryParams(
|
|
|
|
container: {[k: string]: string}, containee: {[k: string]: string}): boolean {
|
|
|
|
return shallowEqual(container, containee);
|
|
|
|
}
|
|
|
|
|
2016-07-25 15:15:07 -04:00
|
|
|
function equalSegmentGroups(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean {
|
|
|
|
if (!equalPath(container.segments, containee.segments)) return false;
|
2016-06-29 19:07:35 -04:00
|
|
|
if (container.numberOfChildren !== containee.numberOfChildren) return false;
|
2016-11-12 08:08:58 -05:00
|
|
|
for (const c in containee.children) {
|
2016-06-15 12:01:05 -04:00
|
|
|
if (!container.children[c]) return false;
|
2016-07-25 15:15:07 -04:00
|
|
|
if (!equalSegmentGroups(container.children[c], containee.children[c])) return false;
|
2016-06-15 12:01:05 -04:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2016-11-11 16:23:47 -05:00
|
|
|
function containsQueryParams(
|
|
|
|
container: {[k: string]: string}, containee: {[k: string]: string}): boolean {
|
2017-01-05 18:56:24 -05:00
|
|
|
return Object.keys(containee).length <= Object.keys(container).length &&
|
2016-11-11 16:23:47 -05:00
|
|
|
Object.keys(containee).every(key => containee[key] === container[key]);
|
|
|
|
}
|
|
|
|
|
2016-07-25 15:15:07 -04:00
|
|
|
function containsSegmentGroup(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean {
|
|
|
|
return containsSegmentGroupHelper(container, containee, containee.segments);
|
2016-06-15 12:01:05 -04:00
|
|
|
}
|
|
|
|
|
2016-07-25 15:15:07 -04:00
|
|
|
function containsSegmentGroupHelper(
|
|
|
|
container: UrlSegmentGroup, containee: UrlSegmentGroup, containeePaths: UrlSegment[]): boolean {
|
|
|
|
if (container.segments.length > containeePaths.length) {
|
|
|
|
const current = container.segments.slice(0, containeePaths.length);
|
2016-06-15 12:01:05 -04:00
|
|
|
if (!equalPath(current, containeePaths)) return false;
|
2016-06-29 19:07:35 -04:00
|
|
|
if (containee.hasChildren()) return false;
|
2016-06-15 12:01:05 -04:00
|
|
|
return true;
|
|
|
|
|
2016-07-25 15:15:07 -04:00
|
|
|
} else if (container.segments.length === containeePaths.length) {
|
|
|
|
if (!equalPath(container.segments, containeePaths)) return false;
|
2016-11-12 08:08:58 -05:00
|
|
|
for (const c in containee.children) {
|
2016-06-15 12:01:05 -04:00
|
|
|
if (!container.children[c]) return false;
|
2016-07-25 15:15:07 -04:00
|
|
|
if (!containsSegmentGroup(container.children[c], containee.children[c])) return false;
|
2016-06-15 12:01:05 -04:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
|
|
|
|
} else {
|
2016-07-25 15:15:07 -04:00
|
|
|
const current = containeePaths.slice(0, container.segments.length);
|
|
|
|
const next = containeePaths.slice(container.segments.length);
|
|
|
|
if (!equalPath(container.segments, current)) return false;
|
2016-06-29 19:07:35 -04:00
|
|
|
if (!container.children[PRIMARY_OUTLET]) return false;
|
2016-07-25 15:15:07 -04:00
|
|
|
return containsSegmentGroupHelper(container.children[PRIMARY_OUTLET], containee, next);
|
2016-06-15 12:01:05 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-24 16:41:37 -04:00
|
|
|
/**
|
2016-09-10 19:53:09 -04:00
|
|
|
* @whatItDoes Represents the parsed URL.
|
|
|
|
*
|
|
|
|
* @howToUse
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* @Component({templateUrl:'template.html'})
|
|
|
|
* class MyComponent {
|
|
|
|
* constructor(router: Router) {
|
|
|
|
* const tree: UrlTree =
|
2016-12-09 13:44:46 -05:00
|
|
|
* router.parseUrl('/team/33/(user/victor//support:help)?debug=true#fragment');
|
2016-09-10 19:53:09 -04:00
|
|
|
* const f = tree.fragment; // return 'fragment'
|
|
|
|
* const q = tree.queryParams; // returns {debug: 'true'}
|
|
|
|
* const g: UrlSegmentGroup = tree.root.children[PRIMARY_OUTLET];
|
|
|
|
* const s: UrlSegment[] = g.segments; // returns 2 segments 'team' and '33'
|
|
|
|
* g.children[PRIMARY_OUTLET].segments; // returns 2 segments 'user' and 'victor'
|
|
|
|
* g.children['support'].segments; // return 1 segment 'help'
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @description
|
|
|
|
*
|
|
|
|
* Since a router state is a tree, and the URL is nothing but a serialized state, the URL is a
|
|
|
|
* serialized tree.
|
|
|
|
* UrlTree is a data structure that provides a lot of affordances in dealing with URLs
|
2016-06-27 15:27:23 -04:00
|
|
|
*
|
2016-06-28 17:49:29 -04:00
|
|
|
* @stable
|
2016-05-24 16:41:37 -04:00
|
|
|
*/
|
2016-06-14 17:55:59 -04:00
|
|
|
export class UrlTree {
|
2017-03-17 13:09:42 -04:00
|
|
|
/** @internal */
|
|
|
|
_queryParamMap: ParamMap;
|
|
|
|
|
2016-12-09 13:44:46 -05:00
|
|
|
/** @internal */
|
2016-06-08 14:13:41 -04:00
|
|
|
constructor(
|
2016-12-09 13:44:46 -05:00
|
|
|
/** The root segment group of the URL tree */
|
2016-09-10 19:53:09 -04:00
|
|
|
public root: UrlSegmentGroup,
|
2016-12-09 13:44:46 -05:00
|
|
|
/** The query params of the URL */
|
2016-09-10 19:53:09 -04:00
|
|
|
public queryParams: {[key: string]: string},
|
2016-12-09 13:44:46 -05:00
|
|
|
/** The fragment of the URL */
|
2017-04-17 14:13:13 -04:00
|
|
|
public fragment: string|null) {}
|
2016-06-09 17:11:54 -04:00
|
|
|
|
2017-03-28 19:17:48 -04:00
|
|
|
get queryParamMap(): ParamMap {
|
2017-03-17 13:09:42 -04:00
|
|
|
if (!this._queryParamMap) {
|
|
|
|
this._queryParamMap = convertToParamMap(this.queryParams);
|
|
|
|
}
|
|
|
|
return this._queryParamMap;
|
|
|
|
}
|
|
|
|
|
2016-12-09 13:44:46 -05:00
|
|
|
/** @docsNotRequired */
|
2017-03-28 19:17:48 -04:00
|
|
|
toString(): string { return DEFAULT_SERIALIZER.serialize(this); }
|
2016-05-21 20:35:55 -04:00
|
|
|
}
|
|
|
|
|
2016-06-28 17:49:29 -04:00
|
|
|
/**
|
2016-12-09 13:44:46 -05:00
|
|
|
* @whatItDoes Represents the parsed URL segment group.
|
2016-09-10 19:53:09 -04:00
|
|
|
*
|
|
|
|
* See {@link UrlTree} for more information.
|
|
|
|
*
|
2016-06-28 17:49:29 -04:00
|
|
|
* @stable
|
|
|
|
*/
|
2016-07-25 15:15:07 -04:00
|
|
|
export class UrlSegmentGroup {
|
2016-12-09 13:44:46 -05:00
|
|
|
/** @internal */
|
2016-07-25 15:15:07 -04:00
|
|
|
_sourceSegment: UrlSegmentGroup;
|
2016-12-09 13:44:46 -05:00
|
|
|
/** @internal */
|
2016-07-25 15:15:07 -04:00
|
|
|
_segmentIndexShift: number;
|
2016-12-09 13:44:46 -05:00
|
|
|
/** The parent node in the url tree */
|
2017-04-17 14:13:13 -04:00
|
|
|
parent: UrlSegmentGroup|null = null;
|
2016-09-10 19:53:09 -04:00
|
|
|
|
|
|
|
constructor(
|
2016-12-09 13:44:46 -05:00
|
|
|
/** The URL segments of this group. See {@link UrlSegment} for more information */
|
2016-09-10 19:53:09 -04:00
|
|
|
public segments: UrlSegment[],
|
2016-12-09 13:44:46 -05:00
|
|
|
/** The list of children of this group */
|
|
|
|
public children: {[key: string]: UrlSegmentGroup}) {
|
2016-06-15 19:45:19 -04:00
|
|
|
forEach(children, (v: any, k: any) => v.parent = this);
|
2016-06-14 17:55:59 -04:00
|
|
|
}
|
2016-05-23 19:14:23 -04:00
|
|
|
|
2017-07-07 19:55:17 -04:00
|
|
|
/** Whether the segment has child segments */
|
2016-06-29 18:26:04 -04:00
|
|
|
hasChildren(): boolean { return this.numberOfChildren > 0; }
|
|
|
|
|
2016-12-09 13:44:46 -05:00
|
|
|
/** Number of child segments */
|
2016-06-29 18:26:04 -04:00
|
|
|
get numberOfChildren(): number { return Object.keys(this.children).length; }
|
2016-06-19 16:45:40 -04:00
|
|
|
|
2016-12-09 13:44:46 -05:00
|
|
|
/** @docsNotRequired */
|
2016-06-14 17:55:59 -04:00
|
|
|
toString(): string { return serializePaths(this); }
|
2016-05-23 19:14:23 -04:00
|
|
|
}
|
|
|
|
|
2016-06-27 15:27:23 -04:00
|
|
|
|
|
|
|
/**
|
2016-09-10 19:53:09 -04:00
|
|
|
* @whatItDoes Represents a single URL segment.
|
|
|
|
*
|
|
|
|
* @howToUse
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* @Component({templateUrl:'template.html'})
|
|
|
|
* class MyComponent {
|
|
|
|
* constructor(router: Router) {
|
|
|
|
* const tree: UrlTree = router.parseUrl('/team;id=33');
|
|
|
|
* const g: UrlSegmentGroup = tree.root.children[PRIMARY_OUTLET];
|
|
|
|
* const s: UrlSegment[] = g.segments;
|
|
|
|
* s[0].path; // returns 'team'
|
|
|
|
* s[0].parameters; // returns {id: 33}
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @description
|
|
|
|
*
|
2016-12-09 13:44:46 -05:00
|
|
|
* A UrlSegment is a part of a URL between the two slashes. It contains a path and the matrix
|
|
|
|
* parameters associated with the segment.
|
2016-09-10 19:53:09 -04:00
|
|
|
*
|
2016-06-28 17:49:29 -04:00
|
|
|
* @stable
|
2016-06-27 15:27:23 -04:00
|
|
|
*/
|
2016-07-25 15:15:07 -04:00
|
|
|
export class UrlSegment {
|
2017-03-17 13:09:42 -04:00
|
|
|
/** @internal */
|
|
|
|
_parameterMap: ParamMap;
|
|
|
|
|
2016-09-10 19:53:09 -04:00
|
|
|
constructor(
|
2016-12-09 13:44:46 -05:00
|
|
|
/** The path part of a URL segment */
|
2016-09-10 19:53:09 -04:00
|
|
|
public path: string,
|
|
|
|
|
2016-12-09 13:44:46 -05:00
|
|
|
/** The matrix parameters associated with a segment */
|
|
|
|
public parameters: {[name: string]: string}) {}
|
2016-09-10 19:53:09 -04:00
|
|
|
|
2017-03-17 13:09:42 -04:00
|
|
|
get parameterMap() {
|
|
|
|
if (!this._parameterMap) {
|
|
|
|
this._parameterMap = convertToParamMap(this.parameters);
|
|
|
|
}
|
|
|
|
return this._parameterMap;
|
|
|
|
}
|
|
|
|
|
2016-12-09 13:44:46 -05:00
|
|
|
/** @docsNotRequired */
|
2016-06-14 17:55:59 -04:00
|
|
|
toString(): string { return serializePath(this); }
|
|
|
|
}
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
export function equalSegments(as: UrlSegment[], bs: UrlSegment[]): boolean {
|
|
|
|
return equalPath(as, bs) && as.every((a, i) => shallowEqual(a.parameters, bs[i].parameters));
|
2016-06-15 12:01:05 -04:00
|
|
|
}
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
export function equalPath(as: UrlSegment[], bs: UrlSegment[]): boolean {
|
|
|
|
if (as.length !== bs.length) return false;
|
|
|
|
return as.every((a, i) => a.path === bs[i].path);
|
2016-05-23 19:14:23 -04:00
|
|
|
}
|
2016-06-14 17:55:59 -04:00
|
|
|
|
|
|
|
export function mapChildrenIntoArray<T>(
|
2016-07-25 15:15:07 -04:00
|
|
|
segment: UrlSegmentGroup, fn: (v: UrlSegmentGroup, k: string) => T[]): T[] {
|
2016-06-15 19:45:19 -04:00
|
|
|
let res: T[] = [];
|
2016-07-25 15:15:07 -04:00
|
|
|
forEach(segment.children, (child: UrlSegmentGroup, childOutlet: string) => {
|
2016-06-14 17:55:59 -04:00
|
|
|
if (childOutlet === PRIMARY_OUTLET) {
|
|
|
|
res = res.concat(fn(child, childOutlet));
|
|
|
|
}
|
|
|
|
});
|
2016-07-25 15:15:07 -04:00
|
|
|
forEach(segment.children, (child: UrlSegmentGroup, childOutlet: string) => {
|
2016-06-14 17:55:59 -04:00
|
|
|
if (childOutlet !== PRIMARY_OUTLET) {
|
|
|
|
res = res.concat(fn(child, childOutlet));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return res;
|
|
|
|
}
|
2016-06-21 14:56:40 -04:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
2016-09-10 19:53:09 -04:00
|
|
|
* @whatItDoes Serializes and deserializes a URL string into a URL tree.
|
|
|
|
*
|
|
|
|
* @description The url serialization strategy is customizable. You can
|
|
|
|
* make all URLs case insensitive by providing a custom UrlSerializer.
|
|
|
|
*
|
|
|
|
* See {@link DefaultUrlSerializer} for an example of a URL serializer.
|
2016-06-27 15:27:23 -04:00
|
|
|
*
|
2016-08-17 18:35:30 -04:00
|
|
|
* @stable
|
2016-06-21 14:56:40 -04:00
|
|
|
*/
|
|
|
|
export abstract class UrlSerializer {
|
2016-12-09 13:44:46 -05:00
|
|
|
/** Parse a url into a {@link UrlTree} */
|
2016-06-21 14:56:40 -04:00
|
|
|
abstract parse(url: string): UrlTree;
|
|
|
|
|
2016-12-09 13:44:46 -05:00
|
|
|
/** Converts a {@link UrlTree} into a url */
|
2016-06-21 14:56:40 -04:00
|
|
|
abstract serialize(tree: UrlTree): string;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-09-10 19:53:09 -04:00
|
|
|
* @whatItDoes A default implementation of the {@link UrlSerializer}.
|
|
|
|
*
|
|
|
|
* @description
|
|
|
|
*
|
|
|
|
* Example URLs:
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* /inbox/33(popup:compose)
|
|
|
|
* /inbox/33;open=true/messages/44
|
|
|
|
* ```
|
|
|
|
*
|
2016-09-12 12:47:44 -04:00
|
|
|
* DefaultUrlSerializer uses parentheses to serialize secondary segments (e.g., popup:compose), the
|
2016-09-10 19:53:09 -04:00
|
|
|
* colon syntax to specify the outlet, and the ';parameter=value' syntax (e.g., open=true) to
|
|
|
|
* specify route specific parameters.
|
2016-06-27 15:27:23 -04:00
|
|
|
*
|
2016-08-17 18:35:30 -04:00
|
|
|
* @stable
|
2016-06-21 14:56:40 -04:00
|
|
|
*/
|
|
|
|
export class DefaultUrlSerializer implements UrlSerializer {
|
2016-12-09 13:44:46 -05:00
|
|
|
/** Parses a url into a {@link UrlTree} */
|
2016-06-21 14:56:40 -04:00
|
|
|
parse(url: string): UrlTree {
|
|
|
|
const p = new UrlParser(url);
|
|
|
|
return new UrlTree(p.parseRootSegment(), p.parseQueryParams(), p.parseFragment());
|
|
|
|
}
|
|
|
|
|
2016-12-09 13:44:46 -05:00
|
|
|
/** Converts a {@link UrlTree} into a url */
|
2016-06-21 14:56:40 -04:00
|
|
|
serialize(tree: UrlTree): string {
|
|
|
|
const segment = `/${serializeSegment(tree.root, true)}`;
|
|
|
|
const query = serializeQueryParams(tree.queryParams);
|
2018-03-09 14:18:45 -05:00
|
|
|
const fragment =
|
|
|
|
typeof tree.fragment === `string` ? `#${encodeUriFragment(tree.fragment !)}` : '';
|
2017-04-02 20:21:56 -04:00
|
|
|
|
2016-06-21 14:56:40 -04:00
|
|
|
return `${segment}${query}${fragment}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-28 19:17:48 -04:00
|
|
|
const DEFAULT_SERIALIZER = new DefaultUrlSerializer();
|
|
|
|
|
2016-07-25 15:15:07 -04:00
|
|
|
export function serializePaths(segment: UrlSegmentGroup): string {
|
|
|
|
return segment.segments.map(p => serializePath(p)).join('/');
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
2016-07-25 15:15:07 -04:00
|
|
|
function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
|
2017-04-02 20:21:56 -04:00
|
|
|
if (!segment.hasChildren()) {
|
|
|
|
return serializePaths(segment);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (root) {
|
2016-07-21 17:50:38 -04:00
|
|
|
const primary = segment.children[PRIMARY_OUTLET] ?
|
|
|
|
serializeSegment(segment.children[PRIMARY_OUTLET], false) :
|
|
|
|
'';
|
2016-06-21 14:56:40 -04:00
|
|
|
const children: string[] = [];
|
2017-04-02 20:21:56 -04:00
|
|
|
|
2016-07-25 15:15:07 -04:00
|
|
|
forEach(segment.children, (v: UrlSegmentGroup, k: string) => {
|
2016-06-21 14:56:40 -04:00
|
|
|
if (k !== PRIMARY_OUTLET) {
|
|
|
|
children.push(`${k}:${serializeSegment(v, false)}`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
return children.length > 0 ? `${primary}(${children.join('//')})` : primary;
|
|
|
|
|
|
|
|
} else {
|
2016-07-25 15:15:07 -04:00
|
|
|
const children = mapChildrenIntoArray(segment, (v: UrlSegmentGroup, k: string) => {
|
2016-06-21 14:56:40 -04:00
|
|
|
if (k === PRIMARY_OUTLET) {
|
|
|
|
return [serializeSegment(segment.children[PRIMARY_OUTLET], false)];
|
|
|
|
}
|
2017-04-02 20:21:56 -04:00
|
|
|
|
|
|
|
return [`${k}:${serializeSegment(v, false)}`];
|
|
|
|
|
2016-06-21 14:56:40 -04:00
|
|
|
});
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
return `${serializePaths(segment)}/(${children.join('//')})`;
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-06 20:10:25 -04:00
|
|
|
/**
|
2018-02-20 18:08:41 -05:00
|
|
|
* Encodes a URI string with the default encoding. This function will only ever be called from
|
|
|
|
* `encodeUriQuery` or `encodeUriSegment` as it's the base set of encodings to be used. We need
|
|
|
|
* a custom encoding because encodeURIComponent is too aggressive and encodes stuff that doesn't
|
2018-03-09 14:18:45 -05:00
|
|
|
* have to be encoded per https://url.spec.whatwg.org.
|
2017-07-06 20:10:25 -04:00
|
|
|
*/
|
2018-02-20 18:08:41 -05:00
|
|
|
function encodeUriString(s: string): string {
|
2017-07-06 20:10:25 -04:00
|
|
|
return encodeURIComponent(s)
|
|
|
|
.replace(/%40/g, '@')
|
|
|
|
.replace(/%3A/gi, ':')
|
|
|
|
.replace(/%24/g, '$')
|
2018-02-20 18:08:41 -05:00
|
|
|
.replace(/%2C/gi, ',');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-03-09 14:18:45 -05:00
|
|
|
* This function should be used to encode both keys and values in a query string key/value. In
|
|
|
|
* the following URL, you need to call encodeUriQuery on "k" and "v":
|
2018-02-20 18:08:41 -05:00
|
|
|
*
|
|
|
|
* http://www.site.org/html;mk=mv?k=v#f
|
|
|
|
*/
|
|
|
|
export function encodeUriQuery(s: string): string {
|
|
|
|
return encodeUriString(s).replace(/%3B/gi, ';');
|
|
|
|
}
|
|
|
|
|
2018-03-09 14:18:45 -05:00
|
|
|
/**
|
|
|
|
* This function should be used to encode a URL fragment. In the following URL, you need to call
|
|
|
|
* encodeUriFragment on "f":
|
|
|
|
*
|
|
|
|
* http://www.site.org/html;mk=mv?k=v#f
|
|
|
|
*/
|
|
|
|
export function encodeUriFragment(s: string): string {
|
|
|
|
return encodeURI(s);
|
|
|
|
}
|
|
|
|
|
2018-02-20 18:08:41 -05:00
|
|
|
/**
|
|
|
|
* This function should be run on any URI segment as well as the key and value in a key/value
|
|
|
|
* pair for matrix params. In the following URL, you need to call encodeUriSegment on "html",
|
|
|
|
* "mk", and "mv":
|
|
|
|
*
|
|
|
|
* http://www.site.org/html;mk=mv?k=v#f
|
|
|
|
*/
|
|
|
|
export function encodeUriSegment(s: string): string {
|
|
|
|
return encodeUriString(s).replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/%26/gi, '&');
|
2016-08-04 20:19:23 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
export function decode(s: string): string {
|
|
|
|
return decodeURIComponent(s);
|
|
|
|
}
|
|
|
|
|
2018-02-20 18:08:41 -05:00
|
|
|
// Query keys/values should have the "+" replaced first, as "+" in a query string is " ".
|
|
|
|
// decodeURIComponent function will not decode "+" as a space.
|
|
|
|
export function decodeQuery(s: string): string {
|
|
|
|
return decode(s.replace(/\+/g, '%20'));
|
|
|
|
}
|
|
|
|
|
2016-07-25 15:15:07 -04:00
|
|
|
export function serializePath(path: UrlSegment): string {
|
2018-02-20 18:08:41 -05:00
|
|
|
return `${encodeUriSegment(path.path)}${serializeMatrixParams(path.parameters)}`;
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
2018-02-20 18:08:41 -05:00
|
|
|
function serializeMatrixParams(params: {[key: string]: string}): string {
|
|
|
|
return Object.keys(params)
|
|
|
|
.map(key => `;${encodeUriSegment(key)}=${encodeUriSegment(params[key])}`)
|
|
|
|
.join('');
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
2016-09-06 07:39:53 -04:00
|
|
|
function serializeQueryParams(params: {[key: string]: any}): string {
|
|
|
|
const strParams: string[] = Object.keys(params).map((name) => {
|
|
|
|
const value = params[name];
|
2018-02-20 18:08:41 -05:00
|
|
|
return Array.isArray(value) ?
|
|
|
|
value.map(v => `${encodeUriQuery(name)}=${encodeUriQuery(v)}`).join('&') :
|
|
|
|
`${encodeUriQuery(name)}=${encodeUriQuery(value)}`;
|
2016-09-06 07:39:53 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
return strParams.length ? `?${strParams.join("&")}` : '';
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
2016-12-08 19:20:04 -05:00
|
|
|
const SEGMENT_RE = /^[^\/()?;=&#]+/;
|
2016-07-25 15:15:07 -04:00
|
|
|
function matchSegments(str: string): string {
|
2016-08-05 12:50:49 -04:00
|
|
|
const match = str.match(SEGMENT_RE);
|
2016-06-21 14:56:40 -04:00
|
|
|
return match ? match[0] : '';
|
|
|
|
}
|
|
|
|
|
2016-12-08 19:20:04 -05:00
|
|
|
const QUERY_PARAM_RE = /^[^=?&#]+/;
|
|
|
|
// Return the name of the query param at the start of the string or an empty string
|
2016-06-21 14:56:40 -04:00
|
|
|
function matchQueryParams(str: string): string {
|
2017-04-04 12:01:39 -04:00
|
|
|
const match = str.match(QUERY_PARAM_RE);
|
2016-06-21 14:56:40 -04:00
|
|
|
return match ? match[0] : '';
|
|
|
|
}
|
|
|
|
|
2016-12-08 19:20:04 -05:00
|
|
|
const QUERY_PARAM_VALUE_RE = /^[^?&#]+/;
|
|
|
|
// Return the value of the query param at the start of the string or an empty string
|
2016-06-21 14:56:40 -04:00
|
|
|
function matchUrlQueryParamValue(str: string): string {
|
2016-08-05 12:50:49 -04:00
|
|
|
const match = str.match(QUERY_PARAM_VALUE_RE);
|
2016-06-21 14:56:40 -04:00
|
|
|
return match ? match[0] : '';
|
|
|
|
}
|
|
|
|
|
|
|
|
class UrlParser {
|
2016-07-21 18:58:21 -04:00
|
|
|
private remaining: string;
|
2016-06-21 14:56:40 -04:00
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
constructor(private url: string) { this.remaining = url; }
|
2016-06-21 14:56:40 -04:00
|
|
|
|
2016-07-25 15:15:07 -04:00
|
|
|
parseRootSegment(): UrlSegmentGroup {
|
2017-04-02 20:21:56 -04:00
|
|
|
this.consumeOptional('/');
|
2016-06-25 15:07:47 -04:00
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
if (this.remaining === '' || this.peekStartsWith('?') || this.peekStartsWith('#')) {
|
2016-07-25 15:15:07 -04:00
|
|
|
return new UrlSegmentGroup([], {});
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
2016-12-09 13:44:46 -05:00
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
// The root segment group never has segments
|
2016-12-09 13:44:46 -05:00
|
|
|
return new UrlSegmentGroup([], this.parseChildren());
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
parseQueryParams(): {[key: string]: any} {
|
|
|
|
const params: {[key: string]: any} = {};
|
|
|
|
if (this.consumeOptional('?')) {
|
|
|
|
do {
|
|
|
|
this.parseQueryParam(params);
|
|
|
|
} while (this.consumeOptional('&'));
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
2017-04-02 20:21:56 -04:00
|
|
|
return params;
|
|
|
|
}
|
2016-06-21 14:56:40 -04:00
|
|
|
|
2017-04-17 14:13:13 -04:00
|
|
|
parseFragment(): string|null {
|
2018-02-20 18:08:41 -05:00
|
|
|
return this.consumeOptional('#') ? decodeURIComponent(this.remaining) : null;
|
2017-04-17 14:13:13 -04:00
|
|
|
}
|
2017-04-02 20:21:56 -04:00
|
|
|
|
|
|
|
private parseChildren(): {[outlet: string]: UrlSegmentGroup} {
|
|
|
|
if (this.remaining === '') {
|
|
|
|
return {};
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
this.consumeOptional('/');
|
|
|
|
|
|
|
|
const segments: UrlSegment[] = [];
|
2016-07-21 17:50:38 -04:00
|
|
|
if (!this.peekStartsWith('(')) {
|
2017-04-02 20:21:56 -04:00
|
|
|
segments.push(this.parseSegment());
|
2016-07-21 17:50:38 -04:00
|
|
|
}
|
2016-06-21 14:56:40 -04:00
|
|
|
|
|
|
|
while (this.peekStartsWith('/') && !this.peekStartsWith('//') && !this.peekStartsWith('/(')) {
|
|
|
|
this.capture('/');
|
2017-04-02 20:21:56 -04:00
|
|
|
segments.push(this.parseSegment());
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
let children: {[outlet: string]: UrlSegmentGroup} = {};
|
2016-06-21 14:56:40 -04:00
|
|
|
if (this.peekStartsWith('/(')) {
|
|
|
|
this.capture('/');
|
|
|
|
children = this.parseParens(true);
|
|
|
|
}
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
let res: {[outlet: string]: UrlSegmentGroup} = {};
|
2016-06-21 14:56:40 -04:00
|
|
|
if (this.peekStartsWith('(')) {
|
|
|
|
res = this.parseParens(false);
|
|
|
|
}
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
if (segments.length > 0 || Object.keys(children).length > 0) {
|
|
|
|
res[PRIMARY_OUTLET] = new UrlSegmentGroup(segments, children);
|
2016-07-21 17:50:38 -04:00
|
|
|
}
|
|
|
|
|
2016-06-21 14:56:40 -04:00
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
// parse a segment with its matrix parameters
|
|
|
|
// ie `name;k1=v1;k2`
|
|
|
|
private parseSegment(): UrlSegment {
|
2016-07-25 15:15:07 -04:00
|
|
|
const path = matchSegments(this.remaining);
|
2016-06-24 14:17:17 -04:00
|
|
|
if (path === '' && this.peekStartsWith(';')) {
|
|
|
|
throw new Error(`Empty path url segment cannot have parameters: '${this.remaining}'.`);
|
|
|
|
}
|
|
|
|
|
2016-06-21 14:56:40 -04:00
|
|
|
this.capture(path);
|
2017-04-02 20:21:56 -04:00
|
|
|
return new UrlSegment(decode(path), this.parseMatrixParams());
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
private parseMatrixParams(): {[key: string]: any} {
|
2016-06-21 14:56:40 -04:00
|
|
|
const params: {[key: string]: any} = {};
|
2017-04-02 20:21:56 -04:00
|
|
|
while (this.consumeOptional(';')) {
|
2016-06-21 14:56:40 -04:00
|
|
|
this.parseParam(params);
|
|
|
|
}
|
|
|
|
return params;
|
|
|
|
}
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
private parseParam(params: {[key: string]: any}): void {
|
2016-07-25 15:15:07 -04:00
|
|
|
const key = matchSegments(this.remaining);
|
2016-06-21 14:56:40 -04:00
|
|
|
if (!key) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.capture(key);
|
2016-08-26 18:41:32 -04:00
|
|
|
let value: any = '';
|
2017-04-02 20:21:56 -04:00
|
|
|
if (this.consumeOptional('=')) {
|
2016-07-25 15:15:07 -04:00
|
|
|
const valueMatch = matchSegments(this.remaining);
|
2016-06-21 14:56:40 -04:00
|
|
|
if (valueMatch) {
|
|
|
|
value = valueMatch;
|
|
|
|
this.capture(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-04 20:19:23 -04:00
|
|
|
params[decode(key)] = decode(value);
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
2016-09-06 07:39:53 -04:00
|
|
|
// Parse a single query parameter `name[=value]`
|
2017-04-02 20:21:56 -04:00
|
|
|
private parseQueryParam(params: {[key: string]: any}): void {
|
2016-06-21 14:56:40 -04:00
|
|
|
const key = matchQueryParams(this.remaining);
|
|
|
|
if (!key) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.capture(key);
|
2016-07-30 17:34:03 -04:00
|
|
|
let value: any = '';
|
2017-04-02 20:21:56 -04:00
|
|
|
if (this.consumeOptional('=')) {
|
2016-11-12 08:08:58 -05:00
|
|
|
const valueMatch = matchUrlQueryParamValue(this.remaining);
|
2016-06-21 14:56:40 -04:00
|
|
|
if (valueMatch) {
|
|
|
|
value = valueMatch;
|
|
|
|
this.capture(value);
|
|
|
|
}
|
|
|
|
}
|
2016-09-06 07:39:53 -04:00
|
|
|
|
2018-02-20 18:08:41 -05:00
|
|
|
const decodedKey = decodeQuery(key);
|
|
|
|
const decodedVal = decodeQuery(value);
|
2016-09-06 07:39:53 -04:00
|
|
|
|
|
|
|
if (params.hasOwnProperty(decodedKey)) {
|
|
|
|
// Append to existing values
|
|
|
|
let currentVal = params[decodedKey];
|
|
|
|
if (!Array.isArray(currentVal)) {
|
|
|
|
currentVal = [currentVal];
|
|
|
|
params[decodedKey] = currentVal;
|
|
|
|
}
|
|
|
|
currentVal.push(decodedVal);
|
|
|
|
} else {
|
|
|
|
// Create a new value
|
|
|
|
params[decodedKey] = decodedVal;
|
|
|
|
}
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
2017-04-02 20:21:56 -04:00
|
|
|
// parse `(a/b//outlet_name:c/d)`
|
|
|
|
private parseParens(allowPrimary: boolean): {[outlet: string]: UrlSegmentGroup} {
|
2016-07-25 15:15:07 -04:00
|
|
|
const segments: {[key: string]: UrlSegmentGroup} = {};
|
2016-06-21 14:56:40 -04:00
|
|
|
this.capture('(');
|
2017-04-02 20:21:56 -04:00
|
|
|
|
|
|
|
while (!this.consumeOptional(')') && this.remaining.length > 0) {
|
2016-07-25 15:15:07 -04:00
|
|
|
const path = matchSegments(this.remaining);
|
2016-07-21 18:58:21 -04:00
|
|
|
|
|
|
|
const next = this.remaining[path.length];
|
|
|
|
|
|
|
|
// if is is not one of these characters, then the segment was unescaped
|
|
|
|
// or the group was not closed
|
|
|
|
if (next !== '/' && next !== ')' && next !== ';') {
|
|
|
|
throw new Error(`Cannot parse url '${this.url}'`);
|
|
|
|
}
|
|
|
|
|
2017-04-17 14:13:13 -04:00
|
|
|
let outletName: string = undefined !;
|
2016-06-21 14:56:40 -04:00
|
|
|
if (path.indexOf(':') > -1) {
|
|
|
|
outletName = path.substr(0, path.indexOf(':'));
|
|
|
|
this.capture(outletName);
|
|
|
|
this.capture(':');
|
|
|
|
} else if (allowPrimary) {
|
|
|
|
outletName = PRIMARY_OUTLET;
|
|
|
|
}
|
|
|
|
|
2016-07-25 15:15:07 -04:00
|
|
|
const children = this.parseChildren();
|
2016-06-21 14:56:40 -04:00
|
|
|
segments[outletName] = Object.keys(children).length === 1 ? children[PRIMARY_OUTLET] :
|
2016-07-25 15:15:07 -04:00
|
|
|
new UrlSegmentGroup([], children);
|
2017-04-02 20:21:56 -04:00
|
|
|
this.consumeOptional('//');
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
2017-04-02 20:21:56 -04:00
|
|
|
|
2016-06-21 14:56:40 -04:00
|
|
|
return segments;
|
|
|
|
}
|
2017-04-02 20:21:56 -04:00
|
|
|
|
|
|
|
private peekStartsWith(str: string): boolean { return this.remaining.startsWith(str); }
|
|
|
|
|
|
|
|
// Consumes the prefix when it is present and returns whether it has been consumed
|
|
|
|
private consumeOptional(str: string): boolean {
|
|
|
|
if (this.peekStartsWith(str)) {
|
|
|
|
this.remaining = this.remaining.substring(str.length);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
private capture(str: string): void {
|
|
|
|
if (!this.consumeOptional(str)) {
|
|
|
|
throw new Error(`Expected "${str}".`);
|
|
|
|
}
|
|
|
|
}
|
2016-06-27 15:27:23 -04:00
|
|
|
}
|