@web-atoms/core
Version:
215 lines (175 loc) • 6.56 kB
text/typescript
import { StringHelper } from "./StringHelper";
export type ISubstitution = string | { variable: string };
export type IVariable = { variable: string, convert: (v: string) => any;};
export class Variable {
public convert: (v: string) => any;
public readonly catchAll: boolean;
public readonly optional: boolean;
public prefix = "";
public suffix = "";
private readonly parseAsNumber: boolean;
public get regex() {
if (this.catchAll) {
return `(/(?<${this.variable}>.+)?)?`;
}
let r = "[^\\/]{1,500}";
if (this.parseAsNumber) {
r = "[0-9]{1,500}";
}
if (this.optional) {
return `(/(?<${this.variable}>${r}))?`;
}
return `/${ StringHelper.escapeRegExp(this.prefix)}(?<${this.variable}>${r})${ StringHelper.escapeRegExp(this.suffix)}`;
}
constructor(public readonly variable: string, public readonly name?: string) {
this.convert = (v) => v;
if (variable.startsWith("*")) {
this.catchAll = true;
variable = variable.substring(1);
}
if (variable.endsWith("?")) {
this.optional = true;
variable = variable.substring(0, variable.length - 1);
}
const index = variable.indexOf(":");
if (index !== -1) {
const parseAs = variable.substring(index + 1);
variable = variable.substring(0, index);
switch(parseAs) {
case "number":
this.parseAsNumber = true;
this.convert = (v) => {
const r = parseFloat(v);
if (Number.isNaN(r)) {
return void 0;
}
return r;
};
break;
case "boolean":
this.convert = (v) => {
if(v === "true") {
return true;
}
if (v === "false") {
return false;
}
};
break;
}
}
this.variable = variable;
this.name ??= variable;
}
}
export default class Route {
/**
* Useful when we want to test urls locally, we can prefix url with `#!` etc
* @param url url to encode
* @returns string
*/
public static encodeUrl(url: string) {
return url;
}
public static create<T>(route: string, queries?: string[], order: number = 0) {
if (!route.startsWith("/")) {
throw new Error("String Route must start with /");
}
return new Route(route, queries, order);
}
public readonly regex: RegExp;
public readonly queries = new Map<string, string>();
public readonly variables: Variable[] = [];
private substitutions: ISubstitution[] = [];
private constructor(
public readonly route: string,
queries: string[],
public readonly order : number = 0) {
if (queries) {
for (const iterator of queries) {
let [name, variable = name] = iterator.split("=");
if (variable.startsWith("{")) {
variable = variable.substring(1, variable.length - 1);
}
this.queries.set(name, variable);
}
}
const tokens = route.substring(1).split(/\//g);
let regex = "^";
this.substitutions.push("/");
let catchAll = false;
for (const iterator of tokens) {
if (regex.length > 2) {
this.substitutions.push("/");
}
const match = /\{(\*?[\p{L}:]{1,50}\??)\}/u.exec(iterator);
if (!match) {
this.substitutions.push(iterator);
regex += StringHelper.escapeRegExp("/");
regex += StringHelper.escapeRegExp(iterator);
continue;
}
const start = match.index;
const index = match[0].length;
const name = match[1];
const prefix = iterator.substring(0, start);
const suffix = iterator.substring(index + 1);
const v = new Variable(name);
v.prefix = prefix;
v.suffix = suffix;
regex += v.regex;
this.variables.push(v);
this.substitutions.push(v);
catchAll ||= v.catchAll;
}
if(!catchAll) {
regex += "\\/?$";
}
this.regex = new RegExp(regex, "i");
}
public matches(route: string, q?: URLSearchParams): any | null {
const matches = this.regex.exec(route);
if (matches?.length > 0) {
const result = {};
const { groups } = matches as any;
for (const iterator of this.variables) {
const v = groups[iterator.variable];
if (v !== void 0 && v !== null) {
const converted = iterator.convert( decodeURIComponent(v));
if (converted === void 0) {
return null;
}
result[iterator.variable] = converted;
}
}
if (q) {
for (let [key, value] of (q as any).entries()) {
const variable = this.queries.get(key) ?? key;
result[variable] = value;
}
}
return result;
}
return null;
}
public substitute(vars: any) {
let result = "";
for (const iterator of this.substitutions) {
if (typeof iterator === "string") {
result += iterator;
continue;
}
result += vars[iterator.variable] ?? "";
}
if (this.queries.size > 0) {
result += "?";
for (const [key, variable] of this.queries.entries()) {
const value = vars[variable];
if (value) {
result += `${encodeURIComponent(key)}=${encodeURIComponent(value)}&`
}
}
}
return result;
}
}