feat(router): update url parser to handle aux routes
This commit is contained in:
parent
073ec0a7eb
commit
fad3b6434c
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue