angular-cn/modules/@angular/router/src/url_tree.ts

456 lines
13 KiB
TypeScript
Raw Normal View History

/**
* @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';
export function createEmptyUrlTree() {
2016-06-14 17:55:59 -04:00
return new UrlTree(new UrlSegment([], {}), {}, null);
}
2016-05-21 20:35:55 -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;
if (container.numberOfChildren !== containee.numberOfChildren) return false;
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;
if (containee.hasChildren()) return false;
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;
if (!container.children[PRIMARY_OUTLET]) return false;
return containsSegmentHelper(container.children[PRIMARY_OUTLET], containee, next);
}
}
2016-05-24 16:41:37 -04:00
/**
* A URL in the tree form.
*
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 {
/**
* @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},
public fragment: string) {}
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 {
/**
* @internal
*/
_sourceSegment: UrlSegment;
/**
* @internal
*/
_pathIndexShift: number;
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}) {
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
/**
* 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-14 17:55:59 -04:00
toString(): string { return serializePaths(this); }
2016-05-23 19:14:23 -04:00
}
/**
2016-06-28 17:49:29 -04:00
* @stable
*/
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;
}
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[] {
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));
}
});
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;
}
/**
* Defines a way to serialize/deserialize a url tree.
*
* @experimental
*/
export abstract class UrlSerializer {
/**
* Parse a url into a {@link UrlTree}
*/
abstract parse(url: string): UrlTree;
/**
* Converts a {@link UrlTree} into a url
*/
abstract serialize(tree: UrlTree): string;
}
/**
* A default implementation of the serialization.
*
* @experimental
*/
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);
const fragment = tree.fragment !== null && tree.fragment !== undefined ?
`#${encodeURIComponent(tree.fragment)}` :
'';
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.hasChildren() && root) {
const primary = segment.children[PRIMARY_OUTLET] ?
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 {
return `${encodeURIComponent(path.path)}${serializeParams(path.parameters)}`;
}
function serializeParams(params: {[key: string]: string}): string {
return pairs(params)
.map(p => `;${encodeURIComponent(p.first)}=${encodeURIComponent(p.second)}`)
.join('');
}
function serializeQueryParams(params: {[key: string]: string}): string {
const strs =
pairs(params).map(p => `${encodeURIComponent(p.first)}=${encodeURIComponent(p.second)}`);
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 {
private remaining: string;
constructor(private url: string) { this.remaining = url; }
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 {
if (this.remaining.startsWith('/')) {
this.capture('/');
}
if (this.remaining === '' || this.remaining.startsWith('?')) {
return new UrlSegment([], {});
} else {
return new UrlSegment([], this.parseSegmentChildren());
}
}
parseSegmentChildren(): {[key: string]: UrlSegment} {
if (this.remaining.length == 0) {
return {};
}
if (this.peekStartsWith('/')) {
this.capture('/');
}
let paths: any[] = [];
if (!this.peekStartsWith('(')) {
paths.push(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);
}
if (paths.length > 0 || Object.keys(children).length > 0) {
res[PRIMARY_OUTLET] = new UrlSegment(paths, children);
}
return res;
}
parsePathWithParams(): UrlPathWithParams {
const path = matchPathWithParams(this.remaining);
if (path === '' && this.peekStartsWith(';')) {
throw new Error(`Empty path url segment cannot have parameters: '${this.remaining}'.`);
}
this.capture(path);
let matrixParams: {[key: string]: any} = {};
if (this.peekStartsWith(';')) {
matrixParams = this.parseMatrixParams();
}
return new UrlPathWithParams(decodeURIComponent(path), matrixParams);
}
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('#')) {
return decodeURIComponent(this.remaining.substring(1));
} 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);
}
}
params[decodeURIComponent(key)] = decodeURIComponent(value);
}
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);
}
}
params[decodeURIComponent(key)] = decodeURIComponent(value);
}
parseParens(allowPrimary: boolean): {[key: string]: UrlSegment} {
const segments: {[key: string]: UrlSegment} = {};
this.capture('(');
while (!this.peekStartsWith(')') && this.remaining.length > 0) {
const path = matchPathWithParams(this.remaining);
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}'`);
}
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;
}
}