From 4b1db0e61cf1b6e77e3176c8fa888833261c084d Mon Sep 17 00:00:00 2001 From: vsavkin Date: Sat, 21 May 2016 17:35:55 -0700 Subject: [PATCH] feat: implement default url serializer --- modules/@angular/router/src/tree.ts | 10 +- modules/@angular/router/src/url_serializer.ts | 227 ++++++++++++++++++ modules/@angular/router/src/url_tree.ts | 11 + .../router/test/url_serializer.spec.ts | 116 +++++++++ 4 files changed, 359 insertions(+), 5 deletions(-) create mode 100644 modules/@angular/router/src/url_serializer.ts create mode 100644 modules/@angular/router/src/url_tree.ts create mode 100644 modules/@angular/router/test/url_serializer.spec.ts diff --git a/modules/@angular/router/src/tree.ts b/modules/@angular/router/src/tree.ts index ecd3c34cee..0d904bce6b 100644 --- a/modules/@angular/router/src/tree.ts +++ b/modules/@angular/router/src/tree.ts @@ -6,17 +6,17 @@ export class Tree { get root(): T { return this._root.value; } - parent(t: T): T { + parent(t: T): T | null { const p = this.pathFromRoot(t); return p.length > 1 ? p[p.length - 2] : null; } children(t: T): T[] { const n = _findNode(t, this._root); - return n ? n.children.map(t => t.value) : null; + return n ? n.children.map(t => t.value) : []; } - firstChild(t: T): T { + firstChild(t: T): T | null { const n = _findNode(t, this._root); return n && n.children.length > 0 ? n.children[0].value : null; } @@ -30,7 +30,7 @@ export function rootNode(tree: Tree): TreeNode { return tree._root; } -function _findNode(expected: T, c: TreeNode): TreeNode { +function _findNode(expected: T, c: TreeNode): TreeNode | null { if (expected === c.value) return c; for (let cc of c.children) { const r = _findNode(expected, cc); @@ -49,7 +49,7 @@ function _findPath(expected: T, c: TreeNode, collected: TreeNode[]): Tr if (r) return r; } - return null; + return []; } function _contains(tree: TreeNode, subtree: TreeNode): boolean { diff --git a/modules/@angular/router/src/url_serializer.ts b/modules/@angular/router/src/url_serializer.ts new file mode 100644 index 0000000000..0a26737df1 --- /dev/null +++ b/modules/@angular/router/src/url_serializer.ts @@ -0,0 +1,227 @@ +import { UrlTree, UrlSegment } from './url_tree'; +import { rootNode, TreeNode } from './tree'; + +/** + * 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 node = serializeUrlTreeNode(rootNode(tree)); + const query = serializeQueryParams(tree.queryParameters); + const fragment = tree.fragment !== null ? `#${tree.fragment}` : ''; + return `${node}${query}${fragment}`; + } +} + +function serializeUrlTreeNode(node: TreeNode): string { + return `${serializeSegment(node.value)}${serializeChildren(node)}`; +} + +function serializeUrlTreeNodes(nodes: TreeNode[]): string { + const primary = serializeSegment(nodes[0].value); + const secondaryNodes = nodes.slice(1); + const secondary = secondaryNodes.length > 0 ? `(${secondaryNodes.map(serializeUrlTreeNode).join("//")})` : ""; + const children = serializeChildren(nodes[0]); + return `${primary}${secondary}${children}`; +} + +function serializeChildren(node: TreeNode): string { + if (node.children.length > 0) { + return `/${serializeUrlTreeNodes(node.children)}`; + } else { + return ""; + } +} + +export function serializeSegment(segment: UrlSegment): string { + return `${segment.segment}${serializeParams(segment.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 = []; + for (let prop in obj) { + if (obj.hasOwnProperty(prop)) { + res.push(new Pair(prop, obj[prop])); + } + } + return res; +} + +const SEGMENT_RE = /^[^\/\(\)\?;=&#]+/; +function matchUrlSegment(str: string): string { + SEGMENT_RE.lastIndex = 0; + var 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(): TreeNode { + if (this.remaining == '' || this.remaining == '/') { + return new TreeNode(new UrlSegment('', {}), []); + } else { + let segments = this.parseSegments(); + return new TreeNode(new UrlSegment('', {}), segments); + } + } + + parseSegments(): TreeNode[] { + if (this.remaining.length == 0) { + return []; + } + if (this.peekStartsWith('/')) { + this.capture('/'); + } + var path = matchUrlSegment(this.remaining); + this.capture(path); + + var matrixParams: {[key: string]: any} = {}; + if (this.peekStartsWith(';')) { + matrixParams = this.parseMatrixParams(); + } + + var secondary = []; + if (this.peekStartsWith('(')) { + secondary = this.parseSecondarySegments(); + } + + var children: TreeNode[] = []; + if (this.peekStartsWith('/') && !this.peekStartsWith('//')) { + this.capture('/'); + children = this.parseSegments(); + } + + let segment = new UrlSegment(path, matrixParams); + let node = new TreeNode(segment, children); + return [node].concat(secondary); + } + + parseQueryParams(): {[key: string]: any} { + var 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 | null { + if (this.peekStartsWith('#')) { + return this.remaining.substring(1); + } else { + return null; + } + } + + 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 (!key) { + return; + } + this.capture(key); + var value: any = "true"; + if (this.peekStartsWith('=')) { + this.capture('='); + var valueMatch = matchUrlSegment(this.remaining); + if (valueMatch) { + value = valueMatch; + this.capture(value); + } + } + + params[key] = value; + } + + parseQueryParam(params: {[key: string]: any}): void { + var key = matchUrlSegment(this.remaining); + if (!key) { + return; + } + this.capture(key); + var value: any = "true"; + if (this.peekStartsWith('=')) { + this.capture('='); + var valueMatch = matchUrlQueryParamValue(this.remaining); + if (valueMatch) { + value = valueMatch; + this.capture(value); + } + } + params[key] = value; + } + + parseSecondarySegments(): TreeNode[] { + var segments = []; + this.capture('('); + + while (!this.peekStartsWith(')') && this.remaining.length > 0) { + segments = segments.concat(this.parseSegments()); + 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 new file mode 100644 index 0000000000..9b9cd78468 --- /dev/null +++ b/modules/@angular/router/src/url_tree.ts @@ -0,0 +1,11 @@ +import { Tree, TreeNode } from './tree'; + +export class UrlTree extends Tree { + constructor(root: TreeNode, public queryParameters: {[key: string]: string}, public fragment: string | null) { + super(root); + } +} + +export class UrlSegment { + constructor(public segment: any, public parameters: {[key: string]: string}) {} +} \ No newline at end of file diff --git a/modules/@angular/router/test/url_serializer.spec.ts b/modules/@angular/router/test/url_serializer.spec.ts new file mode 100644 index 0000000000..f3298be503 --- /dev/null +++ b/modules/@angular/router/test/url_serializer.spec.ts @@ -0,0 +1,116 @@ +import {DefaultUrlSerializer, serializeSegment} from '../src/url_serializer'; +import {UrlSegment} from '../src/url_tree'; + +describe('url serializer', () => { + let url = new DefaultUrlSerializer(); + + it('should parse the root url', () => { + let tree = url.parse("/"); + expectSegment(tree.root, ""); + expect(url.serialize(tree)).toEqual(""); + }); + + it('should parse non-empty urls', () => { + let tree = url.parse("one/two"); + let one = tree.firstChild(tree.root); + expectSegment(one, "one"); + expectSegment(tree.firstChild(one), "two"); + expect(url.serialize(tree)).toEqual("/one/two"); + }); + + it("should parse multiple secondary segments", () => { + let tree = url.parse("/one/two(three//four)/five"); + let c = tree.children(tree.firstChild(tree.root)); + + expectSegment(c[0], "two"); + expectSegment(c[1], "three"); + expectSegment(c[2], "four"); + + expectSegment(tree.firstChild(c[0]), "five"); + + expect(url.serialize(tree)).toEqual("/one/two(three//four)/five"); + }); + + it("should parse secondary segments that have secondary segments", () => { + let tree = url.parse("/one(/two(/three))"); + let c = tree.children(tree.root); + + expectSegment(c[0], "one"); + expectSegment(c[1], "two"); + expectSegment(c[2], "three"); + + expect(url.serialize(tree)).toEqual("/one(two//three)"); + }); + + it("should parse secondary segments that have children", () => { + let tree = url.parse("/one(/two/three)"); + let c = tree.children(tree.root); + + expectSegment(c[0], "one"); + expectSegment(c[1], "two"); + expectSegment(tree.firstChild(c[1]), "three"); + + expect(url.serialize(tree)).toEqual("/one(two/three)"); + }); + + it("should parse an empty secondary segment group", () => { + let tree = url.parse("/one()"); + let c = tree.children(tree.root); + + expectSegment(c[0], "one"); + expect(tree.children(c[0]).length).toEqual(0); + + expect(url.serialize(tree)).toEqual("/one"); + }); + + it("should parse key-value matrix params", () => { + let tree = url.parse("/one;a=11a;b=11b(two;c=22//three;d=33)"); + let c = tree.children(tree.root); + + expectSegment(c[0], "one;a=11a;b=11b"); + expectSegment(c[1], "two;c=22"); + expectSegment(c[2], "three;d=33"); + + expect(url.serialize(tree)).toEqual("/one;a=11a;b=11b(two;c=22//three;d=33)"); + }); + + it("should parse key only matrix params", () => { + let tree = url.parse("/one;a"); + + let c = tree.firstChild(tree.root); + expectSegment(c, "one;a=true"); + + expect(url.serialize(tree)).toEqual("/one;a=true"); + }); + + it("should parse query params", () => { + let tree = url.parse("/one?a=1&b=2"); + expect(tree.queryParameters).toEqual({a: '1', b: '2'}); + }); + + it("should parse key only query params", () => { + let tree = url.parse("/one?a"); + expect(tree.queryParameters).toEqual({a: 'true'}); + }); + + it("should serializer query params", () => { + let tree = url.parse("/one?a"); + expect(url.serialize(tree)).toEqual("/one?a=true"); + }); + + it("should parse fragment", () => { + let tree = url.parse("/one#two"); + expect(tree.fragment).toEqual("two"); + expect(url.serialize(tree)).toEqual("/one#two"); + }); + + it("should parse empty fragment", () => { + let tree = url.parse("/one#"); + expect(tree.fragment).toEqual(""); + expect(url.serialize(tree)).toEqual("/one#"); + }); +}); + +function expectSegment(segment:UrlSegment | null, expected:string):void { + expect(segment ? serializeSegment(segment) : null).toEqual(expected); +}