diff --git a/modules/@angular/router/index.ts b/modules/@angular/router/index.ts index a9d1d07172..5b3909fa8e 100644 --- a/modules/@angular/router/index.ts +++ b/modules/@angular/router/index.ts @@ -10,7 +10,6 @@ export {RouterOutletMap} from './src/router_outlet_map'; export {provideRouter} from './src/router_providers'; export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './src/router_state'; export {PRIMARY_OUTLET, Params} from './src/shared'; -export {DefaultUrlSerializer, UrlSerializer} from './src/url_serializer'; -export {UrlPathWithParams, UrlTree} from './src/url_tree'; +export {DefaultUrlSerializer, UrlPathWithParams, UrlSerializer, UrlTree} from './src/url_tree'; export const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink, RouterLinkActive]; \ No newline at end of file diff --git a/modules/@angular/router/src/common_router_providers.ts b/modules/@angular/router/src/common_router_providers.ts index c42050552f..40f9fbd36e 100644 --- a/modules/@angular/router/src/common_router_providers.ts +++ b/modules/@angular/router/src/common_router_providers.ts @@ -5,7 +5,7 @@ import {RouterConfig} from './config'; import {Router} from './router'; import {RouterOutletMap} from './router_outlet_map'; 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_OPTIONS = new OpaqueToken('ROUTER_OPTIONS'); diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index a8b3c68d5a..329887667d 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -21,8 +21,7 @@ import {resolve} from './resolve'; import {RouterOutletMap} from './router_outlet_map'; import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state'; import {PRIMARY_OUTLET, Params} from './shared'; -import {UrlSerializer} from './url_serializer'; -import {UrlTree, createEmptyUrlTree} from './url_tree'; +import {UrlSerializer, UrlTree, createEmptyUrlTree} from './url_tree'; import {forEach, shallowEqual} from './utils/collection'; import {TreeNode} from './utils/tree'; diff --git a/modules/@angular/router/src/url_serializer.ts b/modules/@angular/router/src/url_serializer.ts deleted file mode 100644 index 62ebad2405..0000000000 --- a/modules/@angular/router/src/url_serializer.ts +++ /dev/null @@ -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 { - constructor(public first: A, public second: B) {} -} -function pairs(obj: {[key: string]: T}): Pair[] { - const res: Pair[] = []; - for (let prop in obj) { - if (obj.hasOwnProperty(prop)) { - res.push(new Pair(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; - } -} diff --git a/modules/@angular/router/src/url_tree.ts b/modules/@angular/router/src/url_tree.ts index 9188a73943..0a4d5eaaf5 100644 --- a/modules/@angular/router/src/url_tree.ts +++ b/modules/@angular/router/src/url_tree.ts @@ -1,5 +1,4 @@ import {PRIMARY_OUTLET} from './shared'; -import {DefaultUrlSerializer, serializePath, serializePaths} from './url_serializer'; import {forEach, shallowEqual} from './utils/collection'; export function createEmptyUrlTree() { @@ -132,3 +131,274 @@ export function mapChildrenIntoArray( }); 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 { + constructor(public first: A, public second: B) {} +} +function pairs(obj: {[key: string]: T}): Pair[] { + const res: Pair[] = []; + for (let prop in obj) { + if (obj.hasOwnProperty(prop)) { + res.push(new Pair(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; + } +} \ No newline at end of file diff --git a/modules/@angular/router/test/apply_redirects.spec.ts b/modules/@angular/router/test/apply_redirects.spec.ts index 6912dfeec9..2790f10a98 100644 --- a/modules/@angular/router/test/apply_redirects.spec.ts +++ b/modules/@angular/router/test/apply_redirects.spec.ts @@ -1,7 +1,6 @@ import {applyRedirects} from '../src/apply_redirects'; import {RouterConfig} from '../src/config'; -import {DefaultUrlSerializer} from '../src/url_serializer'; -import {UrlSegment, UrlTree, equalPathsWithParams} from '../src/url_tree'; +import {DefaultUrlSerializer, UrlSegment, UrlTree, equalPathsWithParams} from '../src/url_tree'; import {TreeNode} from '../src/utils/tree'; describe('applyRedirects', () => { diff --git a/modules/@angular/router/test/create_router_state.spec.ts b/modules/@angular/router/test/create_router_state.spec.ts index f53d66451b..2673118a7e 100644 --- a/modules/@angular/router/test/create_router_state.spec.ts +++ b/modules/@angular/router/test/create_router_state.spec.ts @@ -3,8 +3,7 @@ import {createRouterState} from '../src/create_router_state'; import {recognize} from '../src/recognize'; import {ActivatedRoute, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from '../src/router_state'; import {PRIMARY_OUTLET, Params} from '../src/shared'; -import {DefaultUrlSerializer} from '../src/url_serializer'; -import {UrlSegment, UrlTree} from '../src/url_tree'; +import {DefaultUrlSerializer, UrlSegment, UrlTree} from '../src/url_tree'; import {TreeNode} from '../src/utils/tree'; describe('create router state', () => { diff --git a/modules/@angular/router/test/create_url_tree.spec.ts b/modules/@angular/router/test/create_url_tree.spec.ts index 7731edf45b..cfcacd7e06 100644 --- a/modules/@angular/router/test/create_url_tree.spec.ts +++ b/modules/@angular/router/test/create_url_tree.spec.ts @@ -3,8 +3,7 @@ import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {createUrlTree} from '../src/create_url_tree'; import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../src/router_state'; import {PRIMARY_OUTLET, Params} from '../src/shared'; -import {DefaultUrlSerializer} from '../src/url_serializer'; -import {UrlPathWithParams, UrlSegment, UrlTree} from '../src/url_tree'; +import {DefaultUrlSerializer, UrlPathWithParams, UrlSegment, UrlTree} from '../src/url_tree'; describe('createUrlTree', () => { const serializer = new DefaultUrlSerializer(); diff --git a/modules/@angular/router/test/recognize.spec.ts b/modules/@angular/router/test/recognize.spec.ts index 34b1c12996..3aad6a45c4 100644 --- a/modules/@angular/router/test/recognize.spec.ts +++ b/modules/@angular/router/test/recognize.spec.ts @@ -2,8 +2,7 @@ import {RouterConfig} from '../src/config'; import {recognize} from '../src/recognize'; import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../src/router_state'; import {PRIMARY_OUTLET, Params} from '../src/shared'; -import {DefaultUrlSerializer} from '../src/url_serializer'; -import {UrlTree} from '../src/url_tree'; +import {DefaultUrlSerializer, UrlTree} from '../src/url_tree'; describe('recognize', () => { it('should work', () => { diff --git a/modules/@angular/router/test/resolve.spec.ts b/modules/@angular/router/test/resolve.spec.ts index 439ddf2892..63db7c7919 100644 --- a/modules/@angular/router/test/resolve.spec.ts +++ b/modules/@angular/router/test/resolve.spec.ts @@ -2,8 +2,7 @@ import {RouterConfig} from '../src/config'; import {recognize} from '../src/recognize'; import {resolve} from '../src/resolve'; import {RouterStateSnapshot} from '../src/router_state'; -import {DefaultUrlSerializer} from '../src/url_serializer'; -import {UrlSegment, UrlTree} from '../src/url_tree'; +import {DefaultUrlSerializer, UrlSegment, UrlTree} from '../src/url_tree'; describe('resolve', () => { it('should resolve components', () => { diff --git a/modules/@angular/router/test/url_serializer.spec.ts b/modules/@angular/router/test/url_serializer.spec.ts index bfc348fac9..adf2ef179d 100644 --- a/modules/@angular/router/test/url_serializer.spec.ts +++ b/modules/@angular/router/test/url_serializer.spec.ts @@ -1,6 +1,5 @@ import {PRIMARY_OUTLET} from '../src/shared'; -import {DefaultUrlSerializer, serializePath} from '../src/url_serializer'; -import {UrlSegment} from '../src/url_tree'; +import {DefaultUrlSerializer, UrlSegment, serializePath} from '../src/url_tree'; describe('url serializer', () => { const url = new DefaultUrlSerializer(); diff --git a/modules/@angular/router/test/url_tree.spec.ts b/modules/@angular/router/test/url_tree.spec.ts index 3726afca6b..bf8c7b3a6b 100644 --- a/modules/@angular/router/test/url_tree.spec.ts +++ b/modules/@angular/router/test/url_tree.spec.ts @@ -1,5 +1,4 @@ -import {DefaultUrlSerializer} from '../src/url_serializer'; -import {UrlTree, containsTree} from '../src/url_tree'; +import {DefaultUrlSerializer, UrlTree, containsTree} from '../src/url_tree'; describe('UrlTree', () => { const serializer = new DefaultUrlSerializer(); diff --git a/modules/@angular/router/tsconfig.json b/modules/@angular/router/tsconfig.json index 470db5612d..5f36f8e4f6 100644 --- a/modules/@angular/router/tsconfig.json +++ b/modules/@angular/router/tsconfig.json @@ -28,8 +28,6 @@ "src/config.ts", "src/router_outlet_map.ts", "src/router_state.ts", - "src/url_serializer.ts", - "src/url_tree.ts", "src/shared.ts", "src/common_router_providers.ts", "src/router_providers.ts",