354 lines
9.7 KiB
TypeScript
354 lines
9.7 KiB
TypeScript
export interface RouteDefaults {
|
|
[index: string]: string | null;
|
|
}
|
|
|
|
export interface RouteRequirements {
|
|
[index: string]: string;
|
|
}
|
|
|
|
export interface RouteParams {
|
|
[index: string]: any;
|
|
}
|
|
|
|
export interface QueryParamAddFunction {
|
|
(prefix: string, params: any): void;
|
|
}
|
|
|
|
export interface Route {
|
|
tokens: (string|boolean)[][];
|
|
defaults: undefined[] | RouteDefaults;
|
|
requirements: undefined[] | RouteRequirements;
|
|
hosttokens: string[][];
|
|
schemes: string[];
|
|
methods: string[];
|
|
}
|
|
|
|
export interface RoutesMap {
|
|
[index: string]: Route;
|
|
}
|
|
|
|
export interface Context {
|
|
base_url: string;
|
|
prefix: string;
|
|
host: string;
|
|
port: string | null;
|
|
scheme: string;
|
|
locale: string | null;
|
|
}
|
|
|
|
export interface RoutingData {
|
|
base_url: string;
|
|
routes: RoutesMap;
|
|
prefix?: string;
|
|
host: string;
|
|
port?: string | null;
|
|
scheme?: string;
|
|
locale?: string | null;
|
|
}
|
|
|
|
export class Router {
|
|
private context_: Context;
|
|
private routes_!: RoutesMap;
|
|
|
|
static getInstance(): Router {
|
|
return Routing;
|
|
}
|
|
|
|
static setData(data: RoutingData): void {
|
|
const router = Router.getInstance();
|
|
|
|
router.setRoutingData(data);
|
|
}
|
|
|
|
constructor(context?: Context, routes?: RoutesMap) {
|
|
this.context_ = context || { base_url: '', prefix: '', host: '', port: '', scheme: '', locale: '' };
|
|
this.setRoutes(routes || {});
|
|
}
|
|
|
|
setRoutingData(data: RoutingData): void {
|
|
this.setBaseUrl(data['base_url']);
|
|
this.setRoutes(data['routes']);
|
|
|
|
if (typeof data.prefix !== 'undefined') {
|
|
this.setPrefix(data['prefix']);
|
|
}
|
|
if (typeof data.port !== 'undefined') {
|
|
this.setPort(data['port']);
|
|
}
|
|
if (typeof data.locale !== 'undefined') {
|
|
this.setLocale(data['locale']);
|
|
}
|
|
|
|
this.setHost(data['host']);
|
|
|
|
if (typeof data.scheme !== 'undefined') {
|
|
this.setScheme(data['scheme']);
|
|
}
|
|
}
|
|
|
|
setRoutes(routes: RoutesMap): void {
|
|
this.routes_ = Object.freeze(routes);
|
|
}
|
|
|
|
getRoutes(): RoutesMap {
|
|
return this.routes_;
|
|
}
|
|
|
|
setBaseUrl(baseUrl: string): void {
|
|
this.context_.base_url = baseUrl;
|
|
}
|
|
|
|
getBaseUrl(): string {
|
|
return this.context_.base_url;
|
|
}
|
|
|
|
setPrefix(prefix: string): void {
|
|
this.context_.prefix = prefix;
|
|
}
|
|
|
|
setScheme(scheme: string): void {
|
|
this.context_.scheme = scheme;
|
|
}
|
|
|
|
getScheme(): string {
|
|
return this.context_.scheme;
|
|
}
|
|
|
|
setHost(host: string): void {
|
|
this.context_.host = host;
|
|
}
|
|
|
|
getHost(): string {
|
|
return this.context_.host;
|
|
}
|
|
|
|
setPort(port: string | null) {
|
|
this.context_.port = port;
|
|
}
|
|
|
|
getPort(): string | null {
|
|
return this.context_.port;
|
|
};
|
|
|
|
setLocale(locale: string | null) {
|
|
this.context_.locale = locale;
|
|
}
|
|
|
|
getLocale(): string | null {
|
|
return this.context_.locale;
|
|
};
|
|
|
|
/**
|
|
* Builds query string params added to a URL.
|
|
* Port of jQuery's $.param() function, so credit is due there.
|
|
*/
|
|
buildQueryParams(prefix: string, params: any, add: QueryParamAddFunction): void {
|
|
let name;
|
|
let rbracket = new RegExp(/\[\]$/);
|
|
|
|
if (params instanceof Array) {
|
|
params.forEach((val, i) => {
|
|
if (rbracket.test(prefix)) {
|
|
add(prefix, val);
|
|
} else {
|
|
this.buildQueryParams(prefix + '[' + (typeof val === 'object' ? i : '') + ']', val, add);
|
|
}
|
|
});
|
|
} else if (typeof params === 'object') {
|
|
for (name in params) {
|
|
this.buildQueryParams(prefix + '[' + name + ']', params[name], add);
|
|
}
|
|
} else {
|
|
add(prefix, params);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a raw route object.
|
|
*/
|
|
getRoute(name: string): Route {
|
|
let prefixedName = this.context_.prefix + name;
|
|
let sf41i18nName = name + '.' + this.context_.locale;
|
|
let prefixedSf41i18nName = this.context_.prefix + name + '.' + this.context_.locale;
|
|
let variants = [prefixedName, sf41i18nName, prefixedSf41i18nName, name];
|
|
|
|
for (let i in variants) {
|
|
if (variants[i] in this.routes_) {
|
|
return this.routes_[variants[i]];
|
|
}
|
|
}
|
|
|
|
throw new Error('The route "' + name + '" does not exist.');
|
|
}
|
|
|
|
/**
|
|
* Generates the URL for a route.
|
|
*/
|
|
generate(name: string, opt_params?: RouteParams, absolute?: boolean): string {
|
|
let route = (this.getRoute(name));
|
|
let params = opt_params || {};
|
|
let unusedParams = Object.assign({}, params);
|
|
let url = '';
|
|
let optional = true;
|
|
let host = '';
|
|
let port = (typeof this.getPort() == 'undefined' || this.getPort() === null) ? '' : this.getPort();
|
|
|
|
route.tokens.forEach((token) => {
|
|
if ('text' === token[0] && typeof token[1] === 'string') {
|
|
url = Router.encodePathComponent(token[1]) + url;
|
|
optional = false;
|
|
|
|
return;
|
|
}
|
|
|
|
if ('variable' === token[0]) {
|
|
if (token.length === 6 && token[5] === true) { // Sixth part of the token array indicates if it should be included in case of defaults
|
|
optional = false;
|
|
}
|
|
let hasDefault = route.defaults && !Array.isArray(route.defaults) && typeof token[3] === 'string' && (token[3] in route.defaults);
|
|
if (false === optional || !hasDefault || ((typeof token[3] === 'string' && token[3] in params) && !Array.isArray(route.defaults) && params[token[3]] != route.defaults[token[3]])) {
|
|
let value;
|
|
|
|
if (typeof token[3] === 'string' && token[3] in params) {
|
|
value = params[token[3]];
|
|
delete unusedParams[token[3]];
|
|
} else if (typeof token[3] === 'string' && hasDefault && !Array.isArray(route.defaults)) {
|
|
value = route.defaults[token[3]];
|
|
} else if (optional) {
|
|
return;
|
|
} else {
|
|
throw new Error('The route "' + name + '" requires the parameter "' + token[3] + '".');
|
|
}
|
|
|
|
let empty = true === value || false === value || '' === value;
|
|
|
|
if (!empty || !optional) {
|
|
let encodedValue = Router.encodePathComponent(value);
|
|
|
|
if ('null' === encodedValue && null === value) {
|
|
encodedValue = '';
|
|
}
|
|
|
|
url = token[1] + encodedValue + url;
|
|
}
|
|
|
|
optional = false;
|
|
} else if (hasDefault && (typeof token[3] === 'string' && token[3] in unusedParams)) {
|
|
delete unusedParams[token[3]];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
throw new Error('The token type "' + token[0] + '" is not supported.');
|
|
});
|
|
|
|
if (url === '') {
|
|
url = '/';
|
|
}
|
|
|
|
route.hosttokens.forEach((token) => {
|
|
let value;
|
|
|
|
if ('text' === token[0]) {
|
|
host = token[1] + host;
|
|
|
|
return;
|
|
}
|
|
|
|
if ('variable' === token[0]) {
|
|
if (token[3] in params) {
|
|
value = params[token[3]];
|
|
delete unusedParams[token[3]];
|
|
} else if (route.defaults && !Array.isArray(route.defaults) && (token[3] in route.defaults)) {
|
|
value = route.defaults[token[3]];
|
|
}
|
|
|
|
host = token[1] + value + host;
|
|
}
|
|
});
|
|
|
|
url = this.context_.base_url + url;
|
|
|
|
if (route.requirements && ('_scheme' in route.requirements) && this.getScheme() != route.requirements['_scheme']) {
|
|
const currentHost = host || this.getHost();
|
|
|
|
url = route.requirements['_scheme'] + '://' + currentHost + (currentHost.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url;
|
|
} else if ('undefined' !== typeof route.schemes && 'undefined' !== typeof route.schemes[0] && this.getScheme() !== route.schemes[0]) {
|
|
const currentHost = host || this.getHost();
|
|
|
|
url = route.schemes[0] + '://' + currentHost + (currentHost.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url;
|
|
} else if (host && this.getHost() !== host + (host.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port)) {
|
|
url = this.getScheme() + '://' + host + (host.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url;
|
|
} else if (absolute === true) {
|
|
url = this.getScheme() + '://' + this.getHost() + (this.getHost().indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url;
|
|
}
|
|
|
|
if (Object.keys(unusedParams).length > 0) {
|
|
let queryParams: string[] = [];
|
|
let add = (key: string, value: string|(() => string)) => {
|
|
// if value is a function then call it and assign it's return value as value
|
|
value = (typeof value === 'function') ? value() : value;
|
|
|
|
// change null to empty string
|
|
value = (value === null) ? '' : value;
|
|
|
|
queryParams.push(Router.encodeQueryComponent(key) + '=' + Router.encodeQueryComponent(value));
|
|
};
|
|
|
|
for (const prefix in unusedParams) {
|
|
if(unusedParams.hasOwnProperty(prefix)) {
|
|
this.buildQueryParams(prefix, unusedParams[prefix], add);
|
|
}
|
|
}
|
|
|
|
url = url + '?' + queryParams.join('&');
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* Returns the given string encoded to mimic Symfony URL generator.
|
|
*/
|
|
static customEncodeURIComponent(value: string): string {
|
|
return encodeURIComponent(value)
|
|
.replace(/%2F/g, '/')
|
|
.replace(/%40/g, '@')
|
|
.replace(/%3A/g, ':')
|
|
.replace(/%21/g, '!')
|
|
.replace(/%3B/g, ';')
|
|
.replace(/%2C/g, ',')
|
|
.replace(/%2A/g, '*')
|
|
.replace(/\(/g, '%28')
|
|
.replace(/\)/g, '%29')
|
|
.replace(/'/g, '%27')
|
|
;
|
|
}
|
|
|
|
/**
|
|
* Returns the given path properly encoded to mimic Symfony URL generator.
|
|
*/
|
|
static encodePathComponent(value: string): string {
|
|
return Router.customEncodeURIComponent(value)
|
|
.replace(/%3D/g, '=')
|
|
.replace(/%2B/g, '+')
|
|
.replace(/%21/g, '!')
|
|
.replace(/%7C/g, '|')
|
|
;
|
|
}
|
|
|
|
/**
|
|
* Returns the given query parameter or value properly encoded to mimic Symfony URL generator.
|
|
*/
|
|
static encodeQueryComponent(value: string): string {
|
|
return Router.customEncodeURIComponent(value)
|
|
.replace(/%3F/g, '?')
|
|
;
|
|
}
|
|
}
|
|
|
|
export const Routing = new Router();
|
|
|
|
export default Routing;
|