228 lines
6.1 KiB
TypeScript
228 lines
6.1 KiB
TypeScript
|
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<UrlSegment>): string {
|
||
|
return `${serializeSegment(node.value)}${serializeChildren(node)}`;
|
||
|
}
|
||
|
|
||
|
function serializeUrlTreeNodes(nodes: TreeNode<UrlSegment>[]): 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<UrlSegment>): 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<A,B> { constructor(public first:A, public second:B) {} }
|
||
|
function pairs<T>(obj: {[key: string]: T}):Pair<string,T>[] {
|
||
|
const res = [];
|
||
|
for (let prop in obj) {
|
||
|
if (obj.hasOwnProperty(prop)) {
|
||
|
res.push(new Pair<string, T>(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<UrlSegment> {
|
||
|
if (this.remaining == '' || this.remaining == '/') {
|
||
|
return new TreeNode<UrlSegment>(new UrlSegment('', {}), []);
|
||
|
} else {
|
||
|
let segments = this.parseSegments();
|
||
|
return new TreeNode<UrlSegment>(new UrlSegment('', {}), segments);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
parseSegments(): TreeNode<UrlSegment>[] {
|
||
|
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<UrlSegment>[] = [];
|
||
|
if (this.peekStartsWith('/') && !this.peekStartsWith('//')) {
|
||
|
this.capture('/');
|
||
|
children = this.parseSegments();
|
||
|
}
|
||
|
|
||
|
let segment = new UrlSegment(path, matrixParams);
|
||
|
let node = new TreeNode<UrlSegment>(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<UrlSegment>[] {
|
||
|
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;
|
||
|
}
|
||
|
}
|