diff --git a/modules/angular2/src/alt_router/router_url_parser.ts b/modules/angular2/src/alt_router/router_url_parser.ts index b57662b7c1..fece8a9d2c 100644 --- a/modules/angular2/src/alt_router/router_url_parser.ts +++ b/modules/angular2/src/alt_router/router_url_parser.ts @@ -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; } @@ -8,20 +10,157 @@ export class DefaultRouterUrlParser extends RouterUrlParser { if (url.length === 0) { throw new BaseException(`Invalid url '${url}'`); } - return new Tree(this._parseNodes(url)); + let root = new _UrlParser().parse(url); + return new Tree(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 { + this._remaining = url; + if (url == '' || url == '/') { + return new TreeNode(new UrlSegment('', {}, DEFAULT_OUTLET_NAME), []); } else { - children = []; - currentUrl = url; + return this.parseRoot(); } - return [new UrlSegment(currentUrl, {}, "")].concat(children); } -} \ No newline at end of file + + parseRoot(): TreeNode { + let segments = this.parseSegments(DEFAULT_OUTLET_NAME); + let queryParams = this.peekStartsWith('?') ? this.parseQueryParams() : {}; + return new TreeNode(new UrlSegment('', queryParams, DEFAULT_OUTLET_NAME), segments); + } + + parseSegments(outletName: string): TreeNode[] { + 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[] = []; + if (this.peekStartsWith('/') && !this.peekStartsWith('//')) { + this.capture('/'); + children = this.parseSegments(DEFAULT_OUTLET_NAME); + } + + let segment = new UrlSegment(path, matrixParams, outletName); + let node = new TreeNode(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[] { + 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; + } +} diff --git a/modules/angular2/src/alt_router/segments.ts b/modules/angular2/src/alt_router/segments.ts index 8f7b560d65..4d39f4a851 100644 --- a/modules/angular2/src/alt_router/segments.ts +++ b/modules/angular2/src/alt_router/segments.ts @@ -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 { - constructor(private _nodes: T[]) {} + /** @internal */ + _root: TreeNode; - get root(): T { return this._nodes[0]; } + constructor(root: TreeNode) { 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(tree: Tree): TreeNode { + return tree._root; +} + +function _findNode(expected: T, c: TreeNode): TreeNode { + 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(expected: T, c: TreeNode, collected: TreeNode[]): TreeNode[] { + 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 { + constructor(public value: T, public children: TreeNode[]) {} } 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 { diff --git a/modules/angular2/test/alt_router/router_url_parser_spec.ts b/modules/angular2/test/alt_router/router_url_parser_spec.ts index b7d0b0398e..9d09f15a9c 100644 --- a/modules/angular2/test/alt_router/router_url_parser_spec.ts +++ b/modules/angular2/test/alt_router/router_url_parser_spec.ts @@ -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); } \ No newline at end of file diff --git a/modules/angular2/test/alt_router/tree_spec.ts b/modules/angular2/test/alt_router/tree_spec.ts index 8dd32c8ec6..d2d9d30fe7 100644 --- a/modules/angular2/test/alt_router/tree_spec.ts +++ b/modules/angular2/test/alt_router/tree_spec.ts @@ -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([1, 2, 3]); + let t = new Tree(new TreeNode(1, [])); expect(t.root).toEqual(1); }); it("should return the parent of a node", () => { - let t = new Tree([1, 2, 3]); + let t = new Tree(new TreeNode(1, [new TreeNode(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([1, 2, 3]); + let t = new Tree(new TreeNode(1, [new TreeNode(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(new TreeNode(1, [new TreeNode(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([1, 2, 3]); - expect(t.pathToRoot(2)).toEqual([1, 2]); + let t = new Tree(new TreeNode(1, [new TreeNode(2, [])])); + expect(t.pathFromRoot(2)).toEqual([1, 2]); }); }); } \ No newline at end of file