Skip to content

Commit fad3b64

Browse files
committed
feat(router): update url parser to handle aux routes
1 parent 073ec0a commit fad3b64

File tree

4 files changed

+303
-47
lines changed

4 files changed

+303
-47
lines changed
Lines changed: 152 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import {UrlSegment, Tree} from './segments';
1+
import {UrlSegment, Tree, TreeNode} from './segments';
22
import {BaseException} from 'angular2/src/facade/exceptions';
3+
import {isBlank, isPresent, RegExpWrapper} from 'angular2/src/facade/lang';
4+
import {DEFAULT_OUTLET_NAME} from './constants';
35

46
export abstract class RouterUrlParser { abstract parse(url: string): Tree<UrlSegment>; }
57

@@ -8,20 +10,157 @@ export class DefaultRouterUrlParser extends RouterUrlParser {
810
if (url.length === 0) {
911
throw new BaseException(`Invalid url '${url}'`);
1012
}
11-
return new Tree<UrlSegment>(this._parseNodes(url));
13+
let root = new _UrlParser().parse(url);
14+
return new Tree<UrlSegment>(root);
1215
}
16+
}
1317

14-
private _parseNodes(url: string): UrlSegment[] {
15-
let index = url.indexOf("/", 1);
16-
let children: UrlSegment[];
17-
let currentUrl;
18-
if (index > -1) {
19-
children = this._parseNodes(url.substring(index + 1));
20-
currentUrl = url.substring(0, index);
18+
var SEGMENT_RE = RegExpWrapper.create('^[^\\/\\(\\)\\?;=&#]+');
19+
function matchUrlSegment(str: string): string {
20+
var match = RegExpWrapper.firstMatch(SEGMENT_RE, str);
21+
return isPresent(match) ? match[0] : '';
22+
}
23+
var QUERY_PARAM_VALUE_RE = RegExpWrapper.create('^[^\\(\\)\\?;&#]+');
24+
function matchUrlQueryParamValue(str: string): string {
25+
var match = RegExpWrapper.firstMatch(QUERY_PARAM_VALUE_RE, str);
26+
return isPresent(match) ? match[0] : '';
27+
}
28+
29+
class _UrlParser {
30+
private _remaining: string;
31+
32+
peekStartsWith(str: string): boolean { return this._remaining.startsWith(str); }
33+
34+
capture(str: string): void {
35+
if (!this._remaining.startsWith(str)) {
36+
throw new BaseException(`Expected "${str}".`);
37+
}
38+
this._remaining = this._remaining.substring(str.length);
39+
}
40+
41+
parse(url: string): TreeNode<UrlSegment> {
42+
this._remaining = url;
43+
if (url == '' || url == '/') {
44+
return new TreeNode<UrlSegment>(new UrlSegment('', {}, DEFAULT_OUTLET_NAME), []);
2145
} else {
22-
children = [];
23-
currentUrl = url;
46+
return this.parseRoot();
47+
}
48+
}
49+
50+
parseRoot(): TreeNode<UrlSegment> {
51+
let segments = this.parseSegments(DEFAULT_OUTLET_NAME);
52+
let queryParams = this.peekStartsWith('?') ? this.parseQueryParams() : {};
53+
return new TreeNode<UrlSegment>(new UrlSegment('', queryParams, DEFAULT_OUTLET_NAME), segments);
54+
}
55+
56+
parseSegments(outletName: string): TreeNode<UrlSegment>[] {
57+
if (this._remaining.length == 0) {
58+
return [];
2459
}
25-
return [new UrlSegment(currentUrl, {}, "")].concat(children);
60+
if (this.peekStartsWith('/')) {
61+
this.capture('/');
62+
}
63+
var path = matchUrlSegment(this._remaining);
64+
this.capture(path);
65+
66+
67+
if (path.indexOf(":") > -1) {
68+
let parts = path.split(":");
69+
outletName = parts[0];
70+
path = parts[1];
71+
}
72+
73+
var matrixParams: {[key: string]: any} = {};
74+
if (this.peekStartsWith(';')) {
75+
matrixParams = this.parseMatrixParams();
76+
}
77+
78+
var aux = [];
79+
if (this.peekStartsWith('(')) {
80+
aux = this.parseAuxiliaryRoutes();
81+
}
82+
83+
var children: TreeNode<UrlSegment>[] = [];
84+
if (this.peekStartsWith('/') && !this.peekStartsWith('//')) {
85+
this.capture('/');
86+
children = this.parseSegments(DEFAULT_OUTLET_NAME);
87+
}
88+
89+
let segment = new UrlSegment(path, matrixParams, outletName);
90+
let node = new TreeNode<UrlSegment>(segment, children);
91+
return [node].concat(aux);
92+
}
93+
94+
parseQueryParams(): {[key: string]: any} {
95+
var params: {[key: string]: any} = {};
96+
this.capture('?');
97+
this.parseQueryParam(params);
98+
while (this._remaining.length > 0 && this.peekStartsWith('&')) {
99+
this.capture('&');
100+
this.parseQueryParam(params);
101+
}
102+
return params;
103+
}
104+
105+
parseMatrixParams(): {[key: string]: any} {
106+
var params: {[key: string]: any} = {};
107+
while (this._remaining.length > 0 && this.peekStartsWith(';')) {
108+
this.capture(';');
109+
this.parseParam(params);
110+
}
111+
return params;
112+
}
113+
114+
parseParam(params: {[key: string]: any}): void {
115+
var key = matchUrlSegment(this._remaining);
116+
if (isBlank(key)) {
117+
return;
118+
}
119+
this.capture(key);
120+
var value: any = "true";
121+
if (this.peekStartsWith('=')) {
122+
this.capture('=');
123+
var valueMatch = matchUrlSegment(this._remaining);
124+
if (isPresent(valueMatch)) {
125+
value = valueMatch;
126+
this.capture(value);
127+
}
128+
}
129+
130+
params[key] = value;
131+
}
132+
133+
parseQueryParam(params: {[key: string]: any}): void {
134+
var key = matchUrlSegment(this._remaining);
135+
if (isBlank(key)) {
136+
return;
137+
}
138+
this.capture(key);
139+
var value: any = "true";
140+
if (this.peekStartsWith('=')) {
141+
this.capture('=');
142+
var valueMatch = matchUrlQueryParamValue(this._remaining);
143+
if (isPresent(valueMatch)) {
144+
value = valueMatch;
145+
this.capture(value);
146+
}
147+
}
148+
149+
params[key] = value;
150+
}
151+
152+
parseAuxiliaryRoutes(): TreeNode<UrlSegment>[] {
153+
var segments = [];
154+
this.capture('(');
155+
156+
while (!this.peekStartsWith(')') && this._remaining.length > 0) {
157+
segments = segments.concat(this.parseSegments("aux"));
158+
if (this.peekStartsWith('//')) {
159+
this.capture('//');
160+
}
161+
}
162+
this.capture(')');
163+
164+
return segments;
26165
}
27-
}
166+
}

modules/angular2/src/alt_router/segments.ts

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,77 @@
11
import {ComponentFactory} from 'angular2/core';
22
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
3-
import {Type, isBlank} from 'angular2/src/facade/lang';
3+
import {Type, isBlank, isPresent} from 'angular2/src/facade/lang';
4+
import {DEFAULT_OUTLET_NAME} from './constants';
45

56
export class Tree<T> {
6-
constructor(private _nodes: T[]) {}
7+
/** @internal */
8+
_root: TreeNode<T>;
9+
10+
constructor(root: TreeNode<T>) { this._root = root; }
711

8-
get root(): T { return this._nodes[0]; }
12+
get root(): T { return this._root.value; }
913

1014
parent(t: T): T {
11-
let index = this._nodes.indexOf(t);
12-
return index > 0 ? this._nodes[index - 1] : null;
15+
let p = this.pathFromRoot(t);
16+
return p.length > 1 ? p[p.length - 2] : null;
1317
}
1418

1519
children(t: T): T[] {
16-
let index = this._nodes.indexOf(t);
17-
return index > -1 && index < this._nodes.length - 1 ? [this._nodes[index + 1]] : [];
20+
let n = _findNode(t, this._root);
21+
return isPresent(n) ? n.children.map(t => t.value) : null;
1822
}
1923

2024
firstChild(t: T): T {
21-
let index = this._nodes.indexOf(t);
22-
return index > -1 && index < this._nodes.length - 1 ? this._nodes[index + 1] : null;
25+
let n = _findNode(t, this._root);
26+
return isPresent(n) && n.children.length > 0 ? n.children[0].value : null;
27+
}
28+
29+
pathFromRoot(t: T): T[] { return _findPath(t, this._root, []).map(s => s.value); }
30+
}
31+
32+
export function rootNode<T>(tree: Tree<T>): TreeNode<T> {
33+
return tree._root;
34+
}
35+
36+
function _findNode<T>(expected: T, c: TreeNode<T>): TreeNode<T> {
37+
if (expected === c.value) return c;
38+
for (let cc of c.children) {
39+
let r = _findNode(expected, cc);
40+
if (isPresent(r)) return r;
2341
}
42+
return null;
43+
}
44+
45+
function _findPath<T>(expected: T, c: TreeNode<T>, collected: TreeNode<T>[]): TreeNode<T>[] {
46+
collected.push(c);
2447

25-
pathToRoot(t: T): T[] {
26-
let index = this._nodes.indexOf(t);
27-
return index > -1 ? this._nodes.slice(0, index + 1) : null;
48+
if (expected === c.value) return collected;
49+
for (let cc of c.children) {
50+
let r = _findPath(expected, cc, ListWrapper.clone(collected));
51+
if (isPresent(r)) return r;
2852
}
53+
54+
return null;
55+
}
56+
57+
export class TreeNode<T> {
58+
constructor(public value: T, public children: TreeNode<T>[]) {}
2959
}
3060

3161
export class UrlSegment {
3262
constructor(public segment: string, public parameters: {[key: string]: string},
3363
public outlet: string) {}
64+
65+
toString(): string {
66+
let outletPrefix = this.outlet == DEFAULT_OUTLET_NAME ? "" : `${this.outlet}:`;
67+
return `${outletPrefix}${this.segment}${_serializeParams(this.parameters)}`;
68+
}
69+
}
70+
71+
function _serializeParams(params: {[key: string]: string}): string {
72+
let res = "";
73+
StringMapWrapper.forEach(params, (v, k) => res += `;${k}=${v}`);
74+
return res;
3475
}
3576

3677
export class RouteSegment {
@@ -40,25 +81,23 @@ export class RouteSegment {
4081
/** @internal */
4182
_componentFactory: ComponentFactory;
4283

43-
/** @internal */
44-
_parameters: {[key: string]: string};
45-
46-
constructor(public urlSegments: UrlSegment[], parameters: {[key: string]: string},
84+
constructor(public urlSegments: UrlSegment[], public parameters: {[key: string]: string},
4785
public outlet: string, type: Type, componentFactory: ComponentFactory) {
4886
this._type = type;
4987
this._componentFactory = componentFactory;
50-
this._parameters = parameters;
5188
}
5289

53-
getParam(param: string): string { return this._parameters[param]; }
90+
getParam(param: string): string { return this.parameters[param]; }
5491

5592
get type(): Type { return this._type; }
93+
94+
get stringifiedUrlSegments(): string { return this.urlSegments.map(s => s.toString()).join("/"); }
5695
}
5796

5897
export function equalSegments(a: RouteSegment, b: RouteSegment): boolean {
5998
if (isBlank(a) && !isBlank(b)) return false;
6099
if (!isBlank(a) && isBlank(b)) return false;
61-
return a._type === b._type && StringMapWrapper.equals(a._parameters, b._parameters);
100+
return a._type === b._type && StringMapWrapper.equals(a.parameters, b.parameters);
62101
}
63102

64103
export function routeSegmentComponentFactory(a: RouteSegment): ComponentFactory {

0 commit comments

Comments
 (0)