feat(router): update url parser to handle aux routes

This commit is contained in:
vsavkin 2016-04-25 16:54:51 -07:00 committed by Victor Savkin
parent 073ec0a7eb
commit fad3b6434c
4 changed files with 303 additions and 47 deletions

View File

@ -1,5 +1,7 @@
import {UrlSegment, Tree} from './segments';
import {UrlSegment, Tree, TreeNode} from './segments';
import {BaseException} from 'angular2/src/facade/exceptions';
import {isBlank, isPresent, RegExpWrapper} from 'angular2/src/facade/lang';
import {DEFAULT_OUTLET_NAME} from './constants';
export abstract class RouterUrlParser { abstract parse(url: string): Tree<UrlSegment>; }
@ -8,20 +10,157 @@ export class DefaultRouterUrlParser extends RouterUrlParser {
if (url.length === 0) {
throw new BaseException(`Invalid url '${url}'`);
}
return new Tree<UrlSegment>(this._parseNodes(url));
let root = new _UrlParser().parse(url);
return new Tree<UrlSegment>(root);
}
}
var SEGMENT_RE = RegExpWrapper.create('^[^\\/\\(\\)\\?;=&#]+');
function matchUrlSegment(str: string): string {
var match = RegExpWrapper.firstMatch(SEGMENT_RE, str);
return isPresent(match) ? match[0] : '';
}
var QUERY_PARAM_VALUE_RE = RegExpWrapper.create('^[^\\(\\)\\?;&#]+');
function matchUrlQueryParamValue(str: string): string {
var match = RegExpWrapper.firstMatch(QUERY_PARAM_VALUE_RE, str);
return isPresent(match) ? match[0] : '';
}
class _UrlParser {
private _remaining: string;
peekStartsWith(str: string): boolean { return this._remaining.startsWith(str); }
capture(str: string): void {
if (!this._remaining.startsWith(str)) {
throw new BaseException(`Expected "${str}".`);
}
this._remaining = this._remaining.substring(str.length);
}
private _parseNodes(url: string): UrlSegment[] {
let index = url.indexOf("/", 1);
let children: UrlSegment[];
let currentUrl;
if (index > -1) {
children = this._parseNodes(url.substring(index + 1));
currentUrl = url.substring(0, index);
parse(url: string): TreeNode<UrlSegment> {
this._remaining = url;
if (url == '' || url == '/') {
return new TreeNode<UrlSegment>(new UrlSegment('', {}, DEFAULT_OUTLET_NAME), []);
} else {
children = [];
currentUrl = url;
return this.parseRoot();
}
return [new UrlSegment(currentUrl, {}, "")].concat(children);
}
}
parseRoot(): TreeNode<UrlSegment> {
let segments = this.parseSegments(DEFAULT_OUTLET_NAME);
let queryParams = this.peekStartsWith('?') ? this.parseQueryParams() : {};
return new TreeNode<UrlSegment>(new UrlSegment('', queryParams, DEFAULT_OUTLET_NAME), segments);
}
parseSegments(outletName: string): TreeNode<UrlSegment>[] {
if (this._remaining.length == 0) {
return [];
}
if (this.peekStartsWith('/')) {
this.capture('/');
}
var path = matchUrlSegment(this._remaining);
this.capture(path);
if (path.indexOf(":") > -1) {
let parts = path.split(":");
outletName = parts[0];
path = parts[1];
}
var matrixParams: {[key: string]: any} = {};
if (this.peekStartsWith(';')) {
matrixParams = this.parseMatrixParams();
}
var aux = [];
if (this.peekStartsWith('(')) {
aux = this.parseAuxiliaryRoutes();
}
var children: TreeNode<UrlSegment>[] = [];
if (this.peekStartsWith('/') && !this.peekStartsWith('//')) {
this.capture('/');
children = this.parseSegments(DEFAULT_OUTLET_NAME);
}
let segment = new UrlSegment(path, matrixParams, outletName);
let node = new TreeNode<UrlSegment>(segment, children);
return [node].concat(aux);
}
parseQueryParams(): {[key: string]: any} {
var params: {[key: string]: any} = {};
this.capture('?');
this.parseQueryParam(params);
while (this._remaining.length > 0 && this.peekStartsWith('&')) {
this.capture('&');
this.parseQueryParam(params);
}
return params;
}
parseMatrixParams(): {[key: string]: any} {
var params: {[key: string]: any} = {};
while (this._remaining.length > 0 && this.peekStartsWith(';')) {
this.capture(';');
this.parseParam(params);
}
return params;
}
parseParam(params: {[key: string]: any}): void {
var key = matchUrlSegment(this._remaining);
if (isBlank(key)) {
return;
}
this.capture(key);
var value: any = "true";
if (this.peekStartsWith('=')) {
this.capture('=');
var valueMatch = matchUrlSegment(this._remaining);
if (isPresent(valueMatch)) {
value = valueMatch;
this.capture(value);
}
}
params[key] = value;
}
parseQueryParam(params: {[key: string]: any}): void {
var key = matchUrlSegment(this._remaining);
if (isBlank(key)) {
return;
}
this.capture(key);
var value: any = "true";
if (this.peekStartsWith('=')) {
this.capture('=');
var valueMatch = matchUrlQueryParamValue(this._remaining);
if (isPresent(valueMatch)) {
value = valueMatch;
this.capture(value);
}
}
params[key] = value;
}
parseAuxiliaryRoutes(): TreeNode<UrlSegment>[] {
var segments = [];
this.capture('(');
while (!this.peekStartsWith(')') && this._remaining.length > 0) {
segments = segments.concat(this.parseSegments("aux"));
if (this.peekStartsWith('//')) {
this.capture('//');
}
}
this.capture(')');
return segments;
}
}

View File

@ -1,36 +1,77 @@
import {ComponentFactory} from 'angular2/core';
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {Type, isBlank} from 'angular2/src/facade/lang';
import {Type, isBlank, isPresent} from 'angular2/src/facade/lang';
import {DEFAULT_OUTLET_NAME} from './constants';
export class Tree<T> {
constructor(private _nodes: T[]) {}
/** @internal */
_root: TreeNode<T>;
get root(): T { return this._nodes[0]; }
constructor(root: TreeNode<T>) { this._root = root; }
get root(): T { return this._root.value; }
parent(t: T): T {
let index = this._nodes.indexOf(t);
return index > 0 ? this._nodes[index - 1] : null;
let p = this.pathFromRoot(t);
return p.length > 1 ? p[p.length - 2] : null;
}
children(t: T): T[] {
let index = this._nodes.indexOf(t);
return index > -1 && index < this._nodes.length - 1 ? [this._nodes[index + 1]] : [];
let n = _findNode(t, this._root);
return isPresent(n) ? n.children.map(t => t.value) : null;
}
firstChild(t: T): T {
let index = this._nodes.indexOf(t);
return index > -1 && index < this._nodes.length - 1 ? this._nodes[index + 1] : null;
let n = _findNode(t, this._root);
return isPresent(n) && n.children.length > 0 ? n.children[0].value : null;
}
pathToRoot(t: T): T[] {
let index = this._nodes.indexOf(t);
return index > -1 ? this._nodes.slice(0, index + 1) : null;
pathFromRoot(t: T): T[] { return _findPath(t, this._root, []).map(s => s.value); }
}
export function rootNode<T>(tree: Tree<T>): TreeNode<T> {
return tree._root;
}
function _findNode<T>(expected: T, c: TreeNode<T>): TreeNode<T> {
if (expected === c.value) return c;
for (let cc of c.children) {
let r = _findNode(expected, cc);
if (isPresent(r)) return r;
}
return null;
}
function _findPath<T>(expected: T, c: TreeNode<T>, collected: TreeNode<T>[]): TreeNode<T>[] {
collected.push(c);
if (expected === c.value) return collected;
for (let cc of c.children) {
let r = _findPath(expected, cc, ListWrapper.clone(collected));
if (isPresent(r)) return r;
}
return null;
}
export class TreeNode<T> {
constructor(public value: T, public children: TreeNode<T>[]) {}
}
export class UrlSegment {
constructor(public segment: string, public parameters: {[key: string]: string},
public outlet: string) {}
toString(): string {
let outletPrefix = this.outlet == DEFAULT_OUTLET_NAME ? "" : `${this.outlet}:`;
return `${outletPrefix}${this.segment}${_serializeParams(this.parameters)}`;
}
}
function _serializeParams(params: {[key: string]: string}): string {
let res = "";
StringMapWrapper.forEach(params, (v, k) => res += `;${k}=${v}`);
return res;
}
export class RouteSegment {
@ -40,25 +81,23 @@ export class RouteSegment {
/** @internal */
_componentFactory: ComponentFactory;
/** @internal */
_parameters: {[key: string]: string};
constructor(public urlSegments: UrlSegment[], parameters: {[key: string]: string},
constructor(public urlSegments: UrlSegment[], public parameters: {[key: string]: string},
public outlet: string, type: Type, componentFactory: ComponentFactory) {
this._type = type;
this._componentFactory = componentFactory;
this._parameters = parameters;
}
getParam(param: string): string { return this._parameters[param]; }
getParam(param: string): string { return this.parameters[param]; }
get type(): Type { return this._type; }
get stringifiedUrlSegments(): string { return this.urlSegments.map(s => s.toString()).join("/"); }
}
export function equalSegments(a: RouteSegment, b: RouteSegment): boolean {
if (isBlank(a) && !isBlank(b)) return false;
if (!isBlank(a) && isBlank(b)) return false;
return a._type === b._type && StringMapWrapper.equals(a._parameters, b._parameters);
return a._type === b._type && StringMapWrapper.equals(a.parameters, b.parameters);
}
export function routeSegmentComponentFactory(a: RouteSegment): ComponentFactory {

View File

@ -17,6 +17,7 @@ import {
import {DefaultRouterUrlParser} from 'angular2/src/alt_router/router_url_parser';
import {UrlSegment} from 'angular2/src/alt_router/segments';
import {DEFAULT_OUTLET_NAME} from 'angular2/src/alt_router/constants';
export function main() {
describe('url parsing', () => {
@ -26,20 +27,90 @@ export function main() {
it('should parse the root url', () => {
let tree = parser.parse("/");
expect(tree.root).toEqual(new UrlSegment("/", {}, ""));
expectSegment(tree.root, "");
});
it('should parse non-empty urls', () => {
let tree = parser.parse("one/two/three");
expect(tree.root).toEqual(new UrlSegment("one", {}, ""));
expect(tree.firstChild(tree.root)).toEqual(new UrlSegment("two", {}, ""));
expect(tree.firstChild(tree.firstChild(tree.root))).toEqual(new UrlSegment("three", {}, ""));
let tree = parser.parse("one/two");
expectSegment(tree.firstChild(tree.root), "one");
expectSegment(tree.firstChild(tree.firstChild(tree.root)), "two");
});
it('should parse non-empty absolute urls', () => {
let tree = parser.parse("/one/two");
expect(tree.root).toEqual(new UrlSegment("/one", {}, ""));
expect(tree.firstChild(tree.root)).toEqual(new UrlSegment("two", {}, ""));
it("should parse multiple aux routes", () => {
let tree = parser.parse("/one/two(/three//right:four)/five");
let c = tree.children(tree.firstChild(tree.root));
expectSegment(c[0], "two");
expectSegment(c[1], "aux:three");
expectSegment(c[2], "right:four");
expectSegment(tree.firstChild(c[0]), "five");
});
it("should parse aux routes that have aux routes", () => {
let tree = parser.parse("/one(/two(/three))");
let c = tree.children(tree.root);
expectSegment(c[0], "one");
expectSegment(c[1], "aux:two");
expectSegment(c[2], "aux:three");
});
it("should parse aux routes that have children", () => {
let tree = parser.parse("/one(/two/three)");
let c = tree.children(tree.root);
expectSegment(c[0], "one");
expectSegment(c[1], "aux:two");
expectSegment(tree.firstChild(c[1]), "three");
});
it("should parse an empty aux route definition", () => {
let tree = parser.parse("/one()");
let c = tree.children(tree.root);
expectSegment(c[0], "one");
expect(tree.children(c[0]).length).toEqual(0);
});
it("should parse key-value matrix params", () => {
let tree = parser.parse("/one;a=11a;b=11b(/two;c=22//right:three;d=33)");
let c = tree.children(tree.root);
expectSegment(c[0], "one;a=11a;b=11b");
expectSegment(c[1], "aux:two;c=22");
expectSegment(c[2], "right:three;d=33");
});
it("should parse key only matrix params", () => {
let tree = parser.parse("/one;a");
let c = tree.children(tree.root);
expectSegment(c[0], "one;a=true");
});
it("should parse key-value query params", () => {
let tree = parser.parse("/one?a=1&b=2");
expect(tree.root).toEqual(new UrlSegment("", {'a': '1', 'b': '2'}, DEFAULT_OUTLET_NAME));
});
it("should parse key only query params", () => {
let tree = parser.parse("/one?a");
expect(tree.root).toEqual(new UrlSegment("", {'a': "true"}, DEFAULT_OUTLET_NAME));
});
it("should parse a url with only query params", () => {
let tree = parser.parse("?a");
expect(tree.root).toEqual(new UrlSegment("", {'a': "true"}, DEFAULT_OUTLET_NAME));
});
it("should allow slashes within query params", () => {
let tree = parser.parse("?a=http://boo");
expect(tree.root).toEqual(new UrlSegment("", {'a': "http://boo"}, DEFAULT_OUTLET_NAME));
});
});
}
function expectSegment(segment: UrlSegment, expected: string): void {
expect(segment.toString()).toEqual(expected);
}

View File

@ -15,29 +15,36 @@ import {
xit
} from 'angular2/testing_internal';
import {Tree} from 'angular2/src/alt_router/segments';
import {Tree, TreeNode} from 'angular2/src/alt_router/segments';
export function main() {
describe('tree', () => {
it("should return the root of the tree", () => {
let t = new Tree<any>([1, 2, 3]);
let t = new Tree<any>(new TreeNode<number>(1, []));
expect(t.root).toEqual(1);
});
it("should return the parent of a node", () => {
let t = new Tree<any>([1, 2, 3]);
let t = new Tree<any>(new TreeNode<number>(1, [new TreeNode<number>(2, [])]));
expect(t.parent(1)).toEqual(null);
expect(t.parent(2)).toEqual(1);
});
it("should return the children of a node", () => {
let t = new Tree<any>([1, 2, 3]);
let t = new Tree<any>(new TreeNode<number>(1, [new TreeNode<number>(2, [])]));
expect(t.children(1)).toEqual([2]);
expect(t.children(2)).toEqual([]);
});
it("should return the first child of a node", () => {
let t = new Tree<any>(new TreeNode<number>(1, [new TreeNode<number>(2, [])]));
expect(t.firstChild(1)).toEqual(2);
expect(t.firstChild(2)).toEqual(null);
});
it("should return the path to the root", () => {
let t = new Tree<any>([1, 2, 3]);
expect(t.pathToRoot(2)).toEqual([1, 2]);
let t = new Tree<any>(new TreeNode<number>(1, [new TreeNode<number>(2, [])]));
expect(t.pathFromRoot(2)).toEqual([1, 2]);
});
});
}