refactor(router): removes a circualr dep

This commit is contained in:
vsavkin 2016-06-21 11:56:40 -07:00
parent 8dd3f59c81
commit 15911367a2
13 changed files with 281 additions and 297 deletions

View File

@ -10,7 +10,6 @@ export {RouterOutletMap} from './src/router_outlet_map';
export {provideRouter} from './src/router_providers'; export {provideRouter} from './src/router_providers';
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './src/router_state'; export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './src/router_state';
export {PRIMARY_OUTLET, Params} from './src/shared'; export {PRIMARY_OUTLET, Params} from './src/shared';
export {DefaultUrlSerializer, UrlSerializer} from './src/url_serializer'; export {DefaultUrlSerializer, UrlPathWithParams, UrlSerializer, UrlTree} from './src/url_tree';
export {UrlPathWithParams, UrlTree} from './src/url_tree';
export const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink, RouterLinkActive]; export const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink, RouterLinkActive];

View File

@ -5,7 +5,7 @@ import {RouterConfig} from './config';
import {Router} from './router'; import {Router} from './router';
import {RouterOutletMap} from './router_outlet_map'; import {RouterOutletMap} from './router_outlet_map';
import {ActivatedRoute} from './router_state'; import {ActivatedRoute} from './router_state';
import {DefaultUrlSerializer, UrlSerializer} from './url_serializer'; import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
export const ROUTER_CONFIG = new OpaqueToken('ROUTER_CONFIG'); export const ROUTER_CONFIG = new OpaqueToken('ROUTER_CONFIG');
export const ROUTER_OPTIONS = new OpaqueToken('ROUTER_OPTIONS'); export const ROUTER_OPTIONS = new OpaqueToken('ROUTER_OPTIONS');

View File

@ -21,8 +21,7 @@ import {resolve} from './resolve';
import {RouterOutletMap} from './router_outlet_map'; import {RouterOutletMap} from './router_outlet_map';
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state'; import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state';
import {PRIMARY_OUTLET, Params} from './shared'; import {PRIMARY_OUTLET, Params} from './shared';
import {UrlSerializer} from './url_serializer'; import {UrlSerializer, UrlTree, createEmptyUrlTree} from './url_tree';
import {UrlTree, createEmptyUrlTree} from './url_tree';
import {forEach, shallowEqual} from './utils/collection'; import {forEach, shallowEqual} from './utils/collection';
import {TreeNode} from './utils/tree'; import {TreeNode} from './utils/tree';

View File

@ -1,275 +0,0 @@
import {PRIMARY_OUTLET} from './shared';
import {UrlPathWithParams, UrlSegment, UrlTree, mapChildrenIntoArray} from './url_tree';
import {forEach} from './utils/collection';
/**
* Defines a way to serialize/deserialize a url tree.
*/
export abstract class UrlSerializer {
/**
* Parse a url into a {@Link UrlTree}
*/
abstract parse(url: string): UrlTree;
/**
* Converts a {@Link UrlTree} into a url
*/
abstract serialize(tree: UrlTree): string;
}
/**
* A default implementation of the serialization.
*/
export class DefaultUrlSerializer implements UrlSerializer {
parse(url: string): UrlTree {
const p = new UrlParser(url);
return new UrlTree(p.parseRootSegment(), p.parseQueryParams(), p.parseFragment());
}
serialize(tree: UrlTree): string {
const segment = `/${serializeSegment(tree.root, true)}`;
const query = serializeQueryParams(tree.queryParams);
const fragment = tree.fragment !== null ? `#${tree.fragment}` : '';
return `${segment}${query}${fragment}`;
}
}
export function serializePaths(segment: UrlSegment): string {
return segment.pathsWithParams.map(p => serializePath(p)).join('/');
}
function serializeSegment(segment: UrlSegment, root: boolean): string {
if (segment.children[PRIMARY_OUTLET] && root) {
const primary = serializeSegment(segment.children[PRIMARY_OUTLET], false);
const children: string[] = [];
forEach(segment.children, (v: UrlSegment, k: string) => {
if (k !== PRIMARY_OUTLET) {
children.push(`${k}:${serializeSegment(v, false)}`);
}
});
if (children.length > 0) {
return `${primary}(${children.join('//')})`;
} else {
return `${primary}`;
}
} else if (segment.hasChildren() && !root) {
const children = mapChildrenIntoArray(segment, (v: UrlSegment, k: string) => {
if (k === PRIMARY_OUTLET) {
return [serializeSegment(segment.children[PRIMARY_OUTLET], false)];
} else {
return [`${k}:${serializeSegment(v, false)}`];
}
});
return `${serializePaths(segment)}/(${children.join('//')})`;
} else {
return serializePaths(segment);
}
}
export function serializePath(path: UrlPathWithParams): string {
return `${path.path}${serializeParams(path.parameters)}`;
}
function serializeParams(params: {[key: string]: string}): string {
return pairs(params).map(p => `;${p.first}=${p.second}`).join('');
}
function serializeQueryParams(params: {[key: string]: string}): string {
const strs = pairs(params).map(p => `${p.first}=${p.second}`);
return strs.length > 0 ? `?${strs.join("&")}` : '';
}
class Pair<A, B> {
constructor(public first: A, public second: B) {}
}
function pairs<T>(obj: {[key: string]: T}): Pair<string, T>[] {
const res: Pair<string, T>[] = [];
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
res.push(new Pair<string, T>(prop, obj[prop]));
}
}
return res;
}
const SEGMENT_RE = /^[^\/\(\)\?;=&#]+/;
function matchPathWithParams(str: string): string {
SEGMENT_RE.lastIndex = 0;
const match = SEGMENT_RE.exec(str);
return match ? match[0] : '';
}
const QUERY_PARAM_RE = /^[^=\?&#]+/;
function matchQueryParams(str: string): string {
QUERY_PARAM_RE.lastIndex = 0;
const match = SEGMENT_RE.exec(str);
return match ? match[0] : '';
}
const QUERY_PARAM_VALUE_RE = /^[^\?&#]+/;
function matchUrlQueryParamValue(str: string): string {
QUERY_PARAM_VALUE_RE.lastIndex = 0;
const match = QUERY_PARAM_VALUE_RE.exec(str);
return match ? match[0] : '';
}
class UrlParser {
constructor(private remaining: string) {}
peekStartsWith(str: string): boolean { return this.remaining.startsWith(str); }
capture(str: string): void {
if (!this.remaining.startsWith(str)) {
throw new Error(`Expected "${str}".`);
}
this.remaining = this.remaining.substring(str.length);
}
parseRootSegment(): UrlSegment {
if (this.remaining === '' || this.remaining === '/') {
return new UrlSegment([], {});
} else {
return new UrlSegment([], this.parseSegmentChildren());
}
}
parseSegmentChildren(): {[key: string]: UrlSegment} {
if (this.remaining.length == 0) {
return {};
}
if (this.peekStartsWith('/')) {
this.capture('/');
}
const paths = [this.parsePathWithParams()];
while (this.peekStartsWith('/') && !this.peekStartsWith('//') && !this.peekStartsWith('/(')) {
this.capture('/');
paths.push(this.parsePathWithParams());
}
let children: {[key: string]: UrlSegment} = {};
if (this.peekStartsWith('/(')) {
this.capture('/');
children = this.parseParens(true);
}
let res: {[key: string]: UrlSegment} = {};
if (this.peekStartsWith('(')) {
res = this.parseParens(false);
}
res[PRIMARY_OUTLET] = new UrlSegment(paths, children);
return res;
}
parsePathWithParams(): UrlPathWithParams {
let path = matchPathWithParams(this.remaining);
this.capture(path);
let matrixParams: {[key: string]: any} = {};
if (this.peekStartsWith(';')) {
matrixParams = this.parseMatrixParams();
}
return new UrlPathWithParams(path, matrixParams);
}
parseQueryParams(): {[key: string]: any} {
const params: {[key: string]: any} = {};
if (this.peekStartsWith('?')) {
this.capture('?');
this.parseQueryParam(params);
while (this.remaining.length > 0 && this.peekStartsWith('&')) {
this.capture('&');
this.parseQueryParam(params);
}
}
return params;
}
parseFragment(): string {
if (this.peekStartsWith('#')) {
return this.remaining.substring(1);
} else {
return null;
}
}
parseMatrixParams(): {[key: string]: any} {
const params: {[key: string]: any} = {};
while (this.remaining.length > 0 && this.peekStartsWith(';')) {
this.capture(';');
this.parseParam(params);
}
return params;
}
parseParam(params: {[key: string]: any}): void {
const key = matchPathWithParams(this.remaining);
if (!key) {
return;
}
this.capture(key);
let value: any = 'true';
if (this.peekStartsWith('=')) {
this.capture('=');
const valueMatch = matchPathWithParams(this.remaining);
if (valueMatch) {
value = valueMatch;
this.capture(value);
}
}
params[key] = value;
}
parseQueryParam(params: {[key: string]: any}): void {
const key = matchQueryParams(this.remaining);
if (!key) {
return;
}
this.capture(key);
let value: any = 'true';
if (this.peekStartsWith('=')) {
this.capture('=');
var valueMatch = matchUrlQueryParamValue(this.remaining);
if (valueMatch) {
value = valueMatch;
this.capture(value);
}
}
params[key] = value;
}
parseParens(allowPrimary: boolean): {[key: string]: UrlSegment} {
const segments: {[key: string]: UrlSegment} = {};
this.capture('(');
while (!this.peekStartsWith(')') && this.remaining.length > 0) {
let path = matchPathWithParams(this.remaining);
let outletName: string;
if (path.indexOf(':') > -1) {
outletName = path.substr(0, path.indexOf(':'));
this.capture(outletName);
this.capture(':');
} else if (allowPrimary) {
outletName = PRIMARY_OUTLET;
}
const children = this.parseSegmentChildren();
segments[outletName] = Object.keys(children).length === 1 ? children[PRIMARY_OUTLET] :
new UrlSegment([], children);
if (this.peekStartsWith('//')) {
this.capture('//');
}
}
this.capture(')');
return segments;
}
}

View File

@ -1,5 +1,4 @@
import {PRIMARY_OUTLET} from './shared'; import {PRIMARY_OUTLET} from './shared';
import {DefaultUrlSerializer, serializePath, serializePaths} from './url_serializer';
import {forEach, shallowEqual} from './utils/collection'; import {forEach, shallowEqual} from './utils/collection';
export function createEmptyUrlTree() { export function createEmptyUrlTree() {
@ -132,3 +131,274 @@ export function mapChildrenIntoArray<T>(
}); });
return res; return res;
} }
/**
* Defines a way to serialize/deserialize a url tree.
*/
export abstract class UrlSerializer {
/**
* Parse a url into a {@Link UrlTree}
*/
abstract parse(url: string): UrlTree;
/**
* Converts a {@Link UrlTree} into a url
*/
abstract serialize(tree: UrlTree): string;
}
/**
* A default implementation of the serialization.
*/
export class DefaultUrlSerializer implements UrlSerializer {
parse(url: string): UrlTree {
const p = new UrlParser(url);
return new UrlTree(p.parseRootSegment(), p.parseQueryParams(), p.parseFragment());
}
serialize(tree: UrlTree): string {
const segment = `/${serializeSegment(tree.root, true)}`;
const query = serializeQueryParams(tree.queryParams);
const fragment = tree.fragment !== null ? `#${tree.fragment}` : '';
return `${segment}${query}${fragment}`;
}
}
export function serializePaths(segment: UrlSegment): string {
return segment.pathsWithParams.map(p => serializePath(p)).join('/');
}
function serializeSegment(segment: UrlSegment, root: boolean): string {
if (segment.children[PRIMARY_OUTLET] && root) {
const primary = serializeSegment(segment.children[PRIMARY_OUTLET], false);
const children: string[] = [];
forEach(segment.children, (v: UrlSegment, k: string) => {
if (k !== PRIMARY_OUTLET) {
children.push(`${k}:${serializeSegment(v, false)}`);
}
});
if (children.length > 0) {
return `${primary}(${children.join('//')})`;
} else {
return `${primary}`;
}
} else if (segment.hasChildren() && !root) {
const children = mapChildrenIntoArray(segment, (v: UrlSegment, k: string) => {
if (k === PRIMARY_OUTLET) {
return [serializeSegment(segment.children[PRIMARY_OUTLET], false)];
} else {
return [`${k}:${serializeSegment(v, false)}`];
}
});
return `${serializePaths(segment)}/(${children.join('//')})`;
} else {
return serializePaths(segment);
}
}
export function serializePath(path: UrlPathWithParams): string {
return `${path.path}${serializeParams(path.parameters)}`;
}
function serializeParams(params: {[key: string]: string}): string {
return pairs(params).map(p => `;${p.first}=${p.second}`).join('');
}
function serializeQueryParams(params: {[key: string]: string}): string {
const strs = pairs(params).map(p => `${p.first}=${p.second}`);
return strs.length > 0 ? `?${strs.join("&")}` : '';
}
class Pair<A, B> {
constructor(public first: A, public second: B) {}
}
function pairs<T>(obj: {[key: string]: T}): Pair<string, T>[] {
const res: Pair<string, T>[] = [];
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
res.push(new Pair<string, T>(prop, obj[prop]));
}
}
return res;
}
const SEGMENT_RE = /^[^\/\(\)\?;=&#]+/;
function matchPathWithParams(str: string): string {
SEGMENT_RE.lastIndex = 0;
const match = SEGMENT_RE.exec(str);
return match ? match[0] : '';
}
const QUERY_PARAM_RE = /^[^=\?&#]+/;
function matchQueryParams(str: string): string {
QUERY_PARAM_RE.lastIndex = 0;
const match = SEGMENT_RE.exec(str);
return match ? match[0] : '';
}
const QUERY_PARAM_VALUE_RE = /^[^\?&#]+/;
function matchUrlQueryParamValue(str: string): string {
QUERY_PARAM_VALUE_RE.lastIndex = 0;
const match = QUERY_PARAM_VALUE_RE.exec(str);
return match ? match[0] : '';
}
class UrlParser {
constructor(private remaining: string) {}
peekStartsWith(str: string): boolean { return this.remaining.startsWith(str); }
capture(str: string): void {
if (!this.remaining.startsWith(str)) {
throw new Error(`Expected "${str}".`);
}
this.remaining = this.remaining.substring(str.length);
}
parseRootSegment(): UrlSegment {
if (this.remaining === '' || this.remaining === '/') {
return new UrlSegment([], {});
} else {
return new UrlSegment([], this.parseSegmentChildren());
}
}
parseSegmentChildren(): {[key: string]: UrlSegment} {
if (this.remaining.length == 0) {
return {};
}
if (this.peekStartsWith('/')) {
this.capture('/');
}
const paths = [this.parsePathWithParams()];
while (this.peekStartsWith('/') && !this.peekStartsWith('//') && !this.peekStartsWith('/(')) {
this.capture('/');
paths.push(this.parsePathWithParams());
}
let children: {[key: string]: UrlSegment} = {};
if (this.peekStartsWith('/(')) {
this.capture('/');
children = this.parseParens(true);
}
let res: {[key: string]: UrlSegment} = {};
if (this.peekStartsWith('(')) {
res = this.parseParens(false);
}
res[PRIMARY_OUTLET] = new UrlSegment(paths, children);
return res;
}
parsePathWithParams(): UrlPathWithParams {
let path = matchPathWithParams(this.remaining);
this.capture(path);
let matrixParams: {[key: string]: any} = {};
if (this.peekStartsWith(';')) {
matrixParams = this.parseMatrixParams();
}
return new UrlPathWithParams(path, matrixParams);
}
parseQueryParams(): {[key: string]: any} {
const params: {[key: string]: any} = {};
if (this.peekStartsWith('?')) {
this.capture('?');
this.parseQueryParam(params);
while (this.remaining.length > 0 && this.peekStartsWith('&')) {
this.capture('&');
this.parseQueryParam(params);
}
}
return params;
}
parseFragment(): string {
if (this.peekStartsWith('#')) {
return this.remaining.substring(1);
} else {
return null;
}
}
parseMatrixParams(): {[key: string]: any} {
const params: {[key: string]: any} = {};
while (this.remaining.length > 0 && this.peekStartsWith(';')) {
this.capture(';');
this.parseParam(params);
}
return params;
}
parseParam(params: {[key: string]: any}): void {
const key = matchPathWithParams(this.remaining);
if (!key) {
return;
}
this.capture(key);
let value: any = 'true';
if (this.peekStartsWith('=')) {
this.capture('=');
const valueMatch = matchPathWithParams(this.remaining);
if (valueMatch) {
value = valueMatch;
this.capture(value);
}
}
params[key] = value;
}
parseQueryParam(params: {[key: string]: any}): void {
const key = matchQueryParams(this.remaining);
if (!key) {
return;
}
this.capture(key);
let value: any = 'true';
if (this.peekStartsWith('=')) {
this.capture('=');
var valueMatch = matchUrlQueryParamValue(this.remaining);
if (valueMatch) {
value = valueMatch;
this.capture(value);
}
}
params[key] = value;
}
parseParens(allowPrimary: boolean): {[key: string]: UrlSegment} {
const segments: {[key: string]: UrlSegment} = {};
this.capture('(');
while (!this.peekStartsWith(')') && this.remaining.length > 0) {
let path = matchPathWithParams(this.remaining);
let outletName: string;
if (path.indexOf(':') > -1) {
outletName = path.substr(0, path.indexOf(':'));
this.capture(outletName);
this.capture(':');
} else if (allowPrimary) {
outletName = PRIMARY_OUTLET;
}
const children = this.parseSegmentChildren();
segments[outletName] = Object.keys(children).length === 1 ? children[PRIMARY_OUTLET] :
new UrlSegment([], children);
if (this.peekStartsWith('//')) {
this.capture('//');
}
}
this.capture(')');
return segments;
}
}

View File

@ -1,7 +1,6 @@
import {applyRedirects} from '../src/apply_redirects'; import {applyRedirects} from '../src/apply_redirects';
import {RouterConfig} from '../src/config'; import {RouterConfig} from '../src/config';
import {DefaultUrlSerializer} from '../src/url_serializer'; import {DefaultUrlSerializer, UrlSegment, UrlTree, equalPathsWithParams} from '../src/url_tree';
import {UrlSegment, UrlTree, equalPathsWithParams} from '../src/url_tree';
import {TreeNode} from '../src/utils/tree'; import {TreeNode} from '../src/utils/tree';
describe('applyRedirects', () => { describe('applyRedirects', () => {

View File

@ -3,8 +3,7 @@ import {createRouterState} from '../src/create_router_state';
import {recognize} from '../src/recognize'; import {recognize} from '../src/recognize';
import {ActivatedRoute, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from '../src/router_state'; import {ActivatedRoute, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from '../src/router_state';
import {PRIMARY_OUTLET, Params} from '../src/shared'; import {PRIMARY_OUTLET, Params} from '../src/shared';
import {DefaultUrlSerializer} from '../src/url_serializer'; import {DefaultUrlSerializer, UrlSegment, UrlTree} from '../src/url_tree';
import {UrlSegment, UrlTree} from '../src/url_tree';
import {TreeNode} from '../src/utils/tree'; import {TreeNode} from '../src/utils/tree';
describe('create router state', () => { describe('create router state', () => {

View File

@ -3,8 +3,7 @@ import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {createUrlTree} from '../src/create_url_tree'; import {createUrlTree} from '../src/create_url_tree';
import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../src/router_state'; import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../src/router_state';
import {PRIMARY_OUTLET, Params} from '../src/shared'; import {PRIMARY_OUTLET, Params} from '../src/shared';
import {DefaultUrlSerializer} from '../src/url_serializer'; import {DefaultUrlSerializer, UrlPathWithParams, UrlSegment, UrlTree} from '../src/url_tree';
import {UrlPathWithParams, UrlSegment, UrlTree} from '../src/url_tree';
describe('createUrlTree', () => { describe('createUrlTree', () => {
const serializer = new DefaultUrlSerializer(); const serializer = new DefaultUrlSerializer();

View File

@ -2,8 +2,7 @@ import {RouterConfig} from '../src/config';
import {recognize} from '../src/recognize'; import {recognize} from '../src/recognize';
import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../src/router_state'; import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../src/router_state';
import {PRIMARY_OUTLET, Params} from '../src/shared'; import {PRIMARY_OUTLET, Params} from '../src/shared';
import {DefaultUrlSerializer} from '../src/url_serializer'; import {DefaultUrlSerializer, UrlTree} from '../src/url_tree';
import {UrlTree} from '../src/url_tree';
describe('recognize', () => { describe('recognize', () => {
it('should work', () => { it('should work', () => {

View File

@ -2,8 +2,7 @@ import {RouterConfig} from '../src/config';
import {recognize} from '../src/recognize'; import {recognize} from '../src/recognize';
import {resolve} from '../src/resolve'; import {resolve} from '../src/resolve';
import {RouterStateSnapshot} from '../src/router_state'; import {RouterStateSnapshot} from '../src/router_state';
import {DefaultUrlSerializer} from '../src/url_serializer'; import {DefaultUrlSerializer, UrlSegment, UrlTree} from '../src/url_tree';
import {UrlSegment, UrlTree} from '../src/url_tree';
describe('resolve', () => { describe('resolve', () => {
it('should resolve components', () => { it('should resolve components', () => {

View File

@ -1,6 +1,5 @@
import {PRIMARY_OUTLET} from '../src/shared'; import {PRIMARY_OUTLET} from '../src/shared';
import {DefaultUrlSerializer, serializePath} from '../src/url_serializer'; import {DefaultUrlSerializer, UrlSegment, serializePath} from '../src/url_tree';
import {UrlSegment} from '../src/url_tree';
describe('url serializer', () => { describe('url serializer', () => {
const url = new DefaultUrlSerializer(); const url = new DefaultUrlSerializer();

View File

@ -1,5 +1,4 @@
import {DefaultUrlSerializer} from '../src/url_serializer'; import {DefaultUrlSerializer, UrlTree, containsTree} from '../src/url_tree';
import {UrlTree, containsTree} from '../src/url_tree';
describe('UrlTree', () => { describe('UrlTree', () => {
const serializer = new DefaultUrlSerializer(); const serializer = new DefaultUrlSerializer();

View File

@ -28,8 +28,6 @@
"src/config.ts", "src/config.ts",
"src/router_outlet_map.ts", "src/router_outlet_map.ts",
"src/router_state.ts", "src/router_state.ts",
"src/url_serializer.ts",
"src/url_tree.ts",
"src/shared.ts", "src/shared.ts",
"src/common_router_providers.ts", "src/common_router_providers.ts",
"src/router_providers.ts", "src/router_providers.ts",