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 {