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
|
|
|
|
*/
|
|
|
|
|
2016-06-08 14:13:41 -04:00
|
|
|
import {PRIMARY_OUTLET} 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-06-14 17:55:59 -04:00
|
|
|
return new UrlTree(new UrlSegment([], {}), {}, 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) {
|
|
|
|
return equalSegments(container.root, containee.root);
|
|
|
|
} else {
|
|
|
|
return containsSegment(container.root, containee.root);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function equalSegments(container: UrlSegment, containee: UrlSegment): boolean {
|
|
|
|
if (!equalPath(container.pathsWithParams, containee.pathsWithParams)) return false;
|
2016-06-29 19:07:35 -04:00
|
|
|
if (container.numberOfChildren !== containee.numberOfChildren) return false;
|
2016-06-15 12:01:05 -04:00
|
|
|
for (let c in containee.children) {
|
|
|
|
if (!container.children[c]) return false;
|
|
|
|
if (!equalSegments(container.children[c], containee.children[c])) return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function containsSegment(container: UrlSegment, containee: UrlSegment): boolean {
|
|
|
|
return containsSegmentHelper(container, containee, containee.pathsWithParams);
|
|
|
|
}
|
|
|
|
|
|
|
|
function containsSegmentHelper(
|
|
|
|
container: UrlSegment, containee: UrlSegment, containeePaths: UrlPathWithParams[]): boolean {
|
|
|
|
if (container.pathsWithParams.length > containeePaths.length) {
|
|
|
|
const current = container.pathsWithParams.slice(0, containeePaths.length);
|
|
|
|
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;
|
|
|
|
|
|
|
|
} else if (container.pathsWithParams.length === containeePaths.length) {
|
|
|
|
if (!equalPath(container.pathsWithParams, containeePaths)) return false;
|
|
|
|
for (let c in containee.children) {
|
|
|
|
if (!container.children[c]) return false;
|
|
|
|
if (!containsSegment(container.children[c], containee.children[c])) return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
const current = containeePaths.slice(0, container.pathsWithParams.length);
|
|
|
|
const next = containeePaths.slice(container.pathsWithParams.length);
|
|
|
|
if (!equalPath(container.pathsWithParams, current)) return false;
|
2016-06-29 19:07:35 -04:00
|
|
|
if (!container.children[PRIMARY_OUTLET]) return false;
|
2016-06-15 12:01:05 -04:00
|
|
|
return containsSegmentHelper(container.children[PRIMARY_OUTLET], containee, next);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-24 16:41:37 -04:00
|
|
|
/**
|
|
|
|
* A URL in the tree form.
|
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 {
|
2016-06-02 18:18:34 -04:00
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
2016-06-08 14:13:41 -04:00
|
|
|
constructor(
|
2016-06-14 17:55:59 -04:00
|
|
|
public root: UrlSegment, public queryParams: {[key: string]: string},
|
2016-06-15 12:14:41 -04:00
|
|
|
public fragment: string) {}
|
2016-06-09 17:11:54 -04:00
|
|
|
|
2016-06-09 17:33:09 -04:00
|
|
|
toString(): string { return new DefaultUrlSerializer().serialize(this); }
|
2016-05-21 20:35:55 -04:00
|
|
|
}
|
|
|
|
|
2016-06-28 17:49:29 -04:00
|
|
|
/**
|
|
|
|
* @stable
|
|
|
|
*/
|
2016-05-21 20:35:55 -04:00
|
|
|
export class UrlSegment {
|
2016-06-24 14:17:17 -04:00
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
_sourceSegment: UrlSegment;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
_pathIndexShift: number;
|
|
|
|
|
2016-06-15 12:14:41 -04:00
|
|
|
public parent: UrlSegment = null;
|
2016-06-08 14:13:41 -04:00
|
|
|
constructor(
|
2016-06-14 17:55:59 -04:00
|
|
|
public pathsWithParams: UrlPathWithParams[], public children: {[key: string]: UrlSegment}) {
|
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
|
|
|
|
2016-06-29 18:26:04 -04:00
|
|
|
/**
|
|
|
|
* Return true if the segment has child segments
|
|
|
|
*/
|
|
|
|
hasChildren(): boolean { return this.numberOfChildren > 0; }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the number of child sements.
|
|
|
|
*/
|
|
|
|
get numberOfChildren(): number { return Object.keys(this.children).length; }
|
2016-06-19 16:45:40 -04:00
|
|
|
|
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-06-28 17:49:29 -04:00
|
|
|
* @stable
|
2016-06-27 15:27:23 -04:00
|
|
|
*/
|
2016-06-14 17:55:59 -04:00
|
|
|
export class UrlPathWithParams {
|
|
|
|
constructor(public path: string, public parameters: {[key: string]: string}) {}
|
|
|
|
toString(): string { return serializePath(this); }
|
|
|
|
}
|
|
|
|
|
|
|
|
export function equalPathsWithParams(a: UrlPathWithParams[], b: UrlPathWithParams[]): boolean {
|
2016-05-23 19:14:23 -04:00
|
|
|
if (a.length !== b.length) return false;
|
|
|
|
for (let i = 0; i < a.length; ++i) {
|
|
|
|
if (a[i].path !== b[i].path) return false;
|
|
|
|
if (!shallowEqual(a[i].parameters, b[i].parameters)) return false;
|
2016-06-15 12:01:05 -04:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function equalPath(a: UrlPathWithParams[], b: UrlPathWithParams[]): boolean {
|
|
|
|
if (a.length !== b.length) return false;
|
|
|
|
for (let i = 0; i < a.length; ++i) {
|
|
|
|
if (a[i].path !== b[i].path) return false;
|
2016-05-23 19:14:23 -04:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
2016-06-14 17:55:59 -04:00
|
|
|
|
|
|
|
export function mapChildrenIntoArray<T>(
|
|
|
|
segment: UrlSegment, fn: (v: UrlSegment, k: string) => T[]): T[] {
|
2016-06-15 19:45:19 -04:00
|
|
|
let res: T[] = [];
|
|
|
|
forEach(segment.children, (child: UrlSegment, childOutlet: string) => {
|
2016-06-14 17:55:59 -04:00
|
|
|
if (childOutlet === PRIMARY_OUTLET) {
|
|
|
|
res = res.concat(fn(child, childOutlet));
|
|
|
|
}
|
|
|
|
});
|
2016-06-15 19:45:19 -04:00
|
|
|
forEach(segment.children, (child: UrlSegment, 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
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Defines a way to serialize/deserialize a url tree.
|
2016-06-27 15:27:23 -04:00
|
|
|
*
|
|
|
|
* @experimental
|
2016-06-21 14:56:40 -04:00
|
|
|
*/
|
|
|
|
export abstract class UrlSerializer {
|
|
|
|
/**
|
2016-07-08 02:02:35 -04:00
|
|
|
* Parse a url into a {@link UrlTree}
|
2016-06-21 14:56:40 -04:00
|
|
|
*/
|
|
|
|
abstract parse(url: string): UrlTree;
|
|
|
|
|
|
|
|
/**
|
2016-07-08 02:02:35 -04:00
|
|
|
* Converts a {@link UrlTree} into a url
|
2016-06-21 14:56:40 -04:00
|
|
|
*/
|
|
|
|
abstract serialize(tree: UrlTree): string;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A default implementation of the serialization.
|
2016-06-27 15:27:23 -04:00
|
|
|
*
|
|
|
|
* @experimental
|
2016-06-21 14:56:40 -04:00
|
|
|
*/
|
|
|
|
export class DefaultUrlSerializer implements UrlSerializer {
|
|
|
|
parse(url: string): UrlTree {
|
|
|
|
const p = new UrlParser(url);
|
|
|
|
return new UrlTree(p.parseRootSegment(), p.parseQueryParams(), p.parseFragment());
|
|
|
|
}
|
|
|
|
|
|
|
|
serialize(tree: UrlTree): string {
|
|
|
|
const segment = `/${serializeSegment(tree.root, true)}`;
|
|
|
|
const query = serializeQueryParams(tree.queryParams);
|
2016-07-20 17:30:04 -04:00
|
|
|
const fragment = tree.fragment !== null && tree.fragment !== undefined ?
|
|
|
|
`#${encodeURIComponent(tree.fragment)}` :
|
|
|
|
'';
|
2016-06-21 14:56:40 -04:00
|
|
|
return `${segment}${query}${fragment}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function serializePaths(segment: UrlSegment): string {
|
|
|
|
return segment.pathsWithParams.map(p => serializePath(p)).join('/');
|
|
|
|
}
|
|
|
|
|
|
|
|
function serializeSegment(segment: UrlSegment, root: boolean): string {
|
|
|
|
if (segment.children[PRIMARY_OUTLET] && root) {
|
|
|
|
const primary = serializeSegment(segment.children[PRIMARY_OUTLET], false);
|
|
|
|
const children: string[] = [];
|
|
|
|
forEach(segment.children, (v: UrlSegment, k: string) => {
|
|
|
|
if (k !== PRIMARY_OUTLET) {
|
|
|
|
children.push(`${k}:${serializeSegment(v, false)}`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (children.length > 0) {
|
|
|
|
return `${primary}(${children.join('//')})`;
|
|
|
|
} else {
|
|
|
|
return `${primary}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if (segment.hasChildren() && !root) {
|
|
|
|
const children = mapChildrenIntoArray(segment, (v: UrlSegment, k: string) => {
|
|
|
|
if (k === PRIMARY_OUTLET) {
|
|
|
|
return [serializeSegment(segment.children[PRIMARY_OUTLET], false)];
|
|
|
|
} else {
|
|
|
|
return [`${k}:${serializeSegment(v, false)}`];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return `${serializePaths(segment)}/(${children.join('//')})`;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
return serializePaths(segment);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function serializePath(path: UrlPathWithParams): string {
|
2016-07-07 19:57:25 -04:00
|
|
|
return `${encodeURIComponent(path.path)}${serializeParams(path.parameters)}`;
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function serializeParams(params: {[key: string]: string}): string {
|
2016-07-07 19:57:25 -04:00
|
|
|
return pairs(params)
|
|
|
|
.map(p => `;${encodeURIComponent(p.first)}=${encodeURIComponent(p.second)}`)
|
|
|
|
.join('');
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function serializeQueryParams(params: {[key: string]: string}): string {
|
2016-07-07 19:57:25 -04:00
|
|
|
const strs =
|
|
|
|
pairs(params).map(p => `${encodeURIComponent(p.first)}=${encodeURIComponent(p.second)}`);
|
2016-06-21 14:56:40 -04:00
|
|
|
return strs.length > 0 ? `?${strs.join("&")}` : '';
|
|
|
|
}
|
|
|
|
|
|
|
|
class Pair<A, B> {
|
|
|
|
constructor(public first: A, public second: B) {}
|
|
|
|
}
|
|
|
|
function pairs<T>(obj: {[key: string]: T}): Pair<string, T>[] {
|
|
|
|
const res: Pair<string, T>[] = [];
|
|
|
|
for (let prop in obj) {
|
|
|
|
if (obj.hasOwnProperty(prop)) {
|
|
|
|
res.push(new Pair<string, T>(prop, obj[prop]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
const SEGMENT_RE = /^[^\/\(\)\?;=&#]+/;
|
|
|
|
function matchPathWithParams(str: string): string {
|
|
|
|
SEGMENT_RE.lastIndex = 0;
|
|
|
|
const match = SEGMENT_RE.exec(str);
|
|
|
|
return match ? match[0] : '';
|
|
|
|
}
|
|
|
|
|
|
|
|
const QUERY_PARAM_RE = /^[^=\?&#]+/;
|
|
|
|
function matchQueryParams(str: string): string {
|
|
|
|
QUERY_PARAM_RE.lastIndex = 0;
|
|
|
|
const match = SEGMENT_RE.exec(str);
|
|
|
|
return match ? match[0] : '';
|
|
|
|
}
|
|
|
|
|
|
|
|
const QUERY_PARAM_VALUE_RE = /^[^\?&#]+/;
|
|
|
|
function matchUrlQueryParamValue(str: string): string {
|
|
|
|
QUERY_PARAM_VALUE_RE.lastIndex = 0;
|
|
|
|
const match = QUERY_PARAM_VALUE_RE.exec(str);
|
|
|
|
return match ? match[0] : '';
|
|
|
|
}
|
|
|
|
|
|
|
|
class UrlParser {
|
|
|
|
constructor(private remaining: string) {}
|
|
|
|
|
|
|
|
peekStartsWith(str: string): boolean { return this.remaining.startsWith(str); }
|
|
|
|
|
|
|
|
capture(str: string): void {
|
|
|
|
if (!this.remaining.startsWith(str)) {
|
|
|
|
throw new Error(`Expected "${str}".`);
|
|
|
|
}
|
|
|
|
this.remaining = this.remaining.substring(str.length);
|
|
|
|
}
|
|
|
|
|
|
|
|
parseRootSegment(): UrlSegment {
|
2016-06-25 15:07:47 -04:00
|
|
|
if (this.remaining.startsWith('/')) {
|
|
|
|
this.capture('/');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.remaining === '' || this.remaining.startsWith('?')) {
|
2016-06-21 14:56:40 -04:00
|
|
|
return new UrlSegment([], {});
|
|
|
|
} else {
|
|
|
|
return new UrlSegment([], this.parseSegmentChildren());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parseSegmentChildren(): {[key: string]: UrlSegment} {
|
|
|
|
if (this.remaining.length == 0) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.peekStartsWith('/')) {
|
|
|
|
this.capture('/');
|
|
|
|
}
|
|
|
|
|
|
|
|
const paths = [this.parsePathWithParams()];
|
|
|
|
|
|
|
|
while (this.peekStartsWith('/') && !this.peekStartsWith('//') && !this.peekStartsWith('/(')) {
|
|
|
|
this.capture('/');
|
|
|
|
paths.push(this.parsePathWithParams());
|
|
|
|
}
|
|
|
|
|
|
|
|
let children: {[key: string]: UrlSegment} = {};
|
|
|
|
if (this.peekStartsWith('/(')) {
|
|
|
|
this.capture('/');
|
|
|
|
children = this.parseParens(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
let res: {[key: string]: UrlSegment} = {};
|
|
|
|
if (this.peekStartsWith('(')) {
|
|
|
|
res = this.parseParens(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
res[PRIMARY_OUTLET] = new UrlSegment(paths, children);
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
parsePathWithParams(): UrlPathWithParams {
|
2016-06-24 14:17:17 -04:00
|
|
|
const path = matchPathWithParams(this.remaining);
|
|
|
|
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);
|
|
|
|
let matrixParams: {[key: string]: any} = {};
|
|
|
|
if (this.peekStartsWith(';')) {
|
|
|
|
matrixParams = this.parseMatrixParams();
|
|
|
|
}
|
2016-07-07 19:57:25 -04:00
|
|
|
return new UrlPathWithParams(decodeURIComponent(path), matrixParams);
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
parseQueryParams(): {[key: string]: any} {
|
|
|
|
const params: {[key: string]: any} = {};
|
|
|
|
if (this.peekStartsWith('?')) {
|
|
|
|
this.capture('?');
|
|
|
|
this.parseQueryParam(params);
|
|
|
|
while (this.remaining.length > 0 && this.peekStartsWith('&')) {
|
|
|
|
this.capture('&');
|
|
|
|
this.parseQueryParam(params);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return params;
|
|
|
|
}
|
|
|
|
|
|
|
|
parseFragment(): string {
|
|
|
|
if (this.peekStartsWith('#')) {
|
2016-07-07 19:57:25 -04:00
|
|
|
return decodeURIComponent(this.remaining.substring(1));
|
2016-06-21 14:56:40 -04:00
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
parseMatrixParams(): {[key: string]: any} {
|
|
|
|
const params: {[key: string]: any} = {};
|
|
|
|
while (this.remaining.length > 0 && this.peekStartsWith(';')) {
|
|
|
|
this.capture(';');
|
|
|
|
this.parseParam(params);
|
|
|
|
}
|
|
|
|
return params;
|
|
|
|
}
|
|
|
|
|
|
|
|
parseParam(params: {[key: string]: any}): void {
|
|
|
|
const key = matchPathWithParams(this.remaining);
|
|
|
|
if (!key) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.capture(key);
|
|
|
|
let value: any = 'true';
|
|
|
|
if (this.peekStartsWith('=')) {
|
|
|
|
this.capture('=');
|
|
|
|
const valueMatch = matchPathWithParams(this.remaining);
|
|
|
|
if (valueMatch) {
|
|
|
|
value = valueMatch;
|
|
|
|
this.capture(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-07-07 19:57:25 -04:00
|
|
|
params[decodeURIComponent(key)] = decodeURIComponent(value);
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
parseQueryParam(params: {[key: string]: any}): void {
|
|
|
|
const key = matchQueryParams(this.remaining);
|
|
|
|
if (!key) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.capture(key);
|
|
|
|
let value: any = 'true';
|
|
|
|
if (this.peekStartsWith('=')) {
|
|
|
|
this.capture('=');
|
|
|
|
var valueMatch = matchUrlQueryParamValue(this.remaining);
|
|
|
|
if (valueMatch) {
|
|
|
|
value = valueMatch;
|
|
|
|
this.capture(value);
|
|
|
|
}
|
|
|
|
}
|
2016-07-07 19:57:25 -04:00
|
|
|
params[decodeURIComponent(key)] = decodeURIComponent(value);
|
2016-06-21 14:56:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
parseParens(allowPrimary: boolean): {[key: string]: UrlSegment} {
|
|
|
|
const segments: {[key: string]: UrlSegment} = {};
|
|
|
|
this.capture('(');
|
|
|
|
|
|
|
|
while (!this.peekStartsWith(')') && this.remaining.length > 0) {
|
|
|
|
let path = matchPathWithParams(this.remaining);
|
|
|
|
let outletName: string;
|
|
|
|
if (path.indexOf(':') > -1) {
|
|
|
|
outletName = path.substr(0, path.indexOf(':'));
|
|
|
|
this.capture(outletName);
|
|
|
|
this.capture(':');
|
|
|
|
} else if (allowPrimary) {
|
|
|
|
outletName = PRIMARY_OUTLET;
|
|
|
|
}
|
|
|
|
|
|
|
|
const children = this.parseSegmentChildren();
|
|
|
|
segments[outletName] = Object.keys(children).length === 1 ? children[PRIMARY_OUTLET] :
|
|
|
|
new UrlSegment([], children);
|
|
|
|
|
|
|
|
if (this.peekStartsWith('//')) {
|
|
|
|
this.capture('//');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.capture(')');
|
|
|
|
|
|
|
|
return segments;
|
|
|
|
}
|
2016-06-27 15:27:23 -04:00
|
|
|
}
|