265 lines
7.9 KiB
JavaScript
265 lines
7.9 KiB
JavaScript
const NullProtoObj = /* @__PURE__ */ (() => {
|
|
const e = function() {};
|
|
return e.prototype = Object.create(null), Object.freeze(e.prototype), e;
|
|
})();
|
|
|
|
/**
|
|
* Create a new router context.
|
|
*/
|
|
function createRouter() {
|
|
return {
|
|
root: { key: "" },
|
|
static: new NullProtoObj()
|
|
};
|
|
}
|
|
|
|
function splitPath(path) {
|
|
const [_, ...s] = path.split("/");
|
|
return s[s.length - 1] === "" ? s.slice(0, -1) : s;
|
|
}
|
|
function getMatchParams(segments, paramsMap) {
|
|
const params = new NullProtoObj();
|
|
for (const [index, name] of paramsMap) {
|
|
const segment = index < 0 ? segments.slice(-(index + 1)).join("/") : segments[index];
|
|
if (typeof name === "string") params[name] = segment;
|
|
else {
|
|
const match = segment.match(name);
|
|
if (match) for (const key in match.groups) params[key] = match.groups[key];
|
|
}
|
|
}
|
|
return params;
|
|
}
|
|
|
|
/**
|
|
* Add a route to the router context.
|
|
*/
|
|
function addRoute(ctx, method = "", path, data) {
|
|
method = method.toUpperCase();
|
|
if (path.charCodeAt(0) !== 47) path = `/${path}`;
|
|
path = path.replace(/\\:/g, "%3A");
|
|
const segments = splitPath(path);
|
|
let node = ctx.root;
|
|
let _unnamedParamIndex = 0;
|
|
const paramsMap = [];
|
|
const paramsRegexp = [];
|
|
for (let i = 0; i < segments.length; i++) {
|
|
let segment = segments[i];
|
|
if (segment.startsWith("**")) {
|
|
if (!node.wildcard) node.wildcard = { key: "**" };
|
|
node = node.wildcard;
|
|
paramsMap.push([
|
|
-(i + 1),
|
|
segment.split(":")[1] || "_",
|
|
segment.length === 2
|
|
]);
|
|
break;
|
|
}
|
|
if (segment === "*" || segment.includes(":")) {
|
|
if (!node.param) node.param = { key: "*" };
|
|
node = node.param;
|
|
if (segment === "*") paramsMap.push([
|
|
i,
|
|
`_${_unnamedParamIndex++}`,
|
|
true
|
|
]);
|
|
else if (segment.includes(":", 1)) {
|
|
const regexp = getParamRegexp(segment);
|
|
paramsRegexp[i] = regexp;
|
|
node.hasRegexParam = true;
|
|
paramsMap.push([
|
|
i,
|
|
regexp,
|
|
false
|
|
]);
|
|
} else paramsMap.push([
|
|
i,
|
|
segment.slice(1),
|
|
false
|
|
]);
|
|
continue;
|
|
}
|
|
if (segment === "\\*") segment = segments[i] = "*";
|
|
else if (segment === "\\*\\*") segment = segments[i] = "**";
|
|
const child = node.static?.[segment];
|
|
if (child) node = child;
|
|
else {
|
|
const staticNode = { key: segment };
|
|
if (!node.static) node.static = new NullProtoObj();
|
|
node.static[segment] = staticNode;
|
|
node = staticNode;
|
|
}
|
|
}
|
|
const hasParams = paramsMap.length > 0;
|
|
if (!node.methods) node.methods = new NullProtoObj();
|
|
node.methods[method] ??= [];
|
|
node.methods[method].push({
|
|
data: data || null,
|
|
paramsRegexp,
|
|
paramsMap: hasParams ? paramsMap : void 0
|
|
});
|
|
if (!hasParams) ctx.static["/" + segments.join("/")] = node;
|
|
}
|
|
function getParamRegexp(segment) {
|
|
const regex = segment.replace(/:(\w+)/g, (_, id) => `(?<${id}>[^/]+)`).replace(/\./g, "\\.");
|
|
return /* @__PURE__ */ new RegExp(`^${regex}$`);
|
|
}
|
|
|
|
/**
|
|
* Find a route by path.
|
|
*/
|
|
function findRoute(ctx, method = "", path, opts) {
|
|
if (path.charCodeAt(path.length - 1) === 47) path = path.slice(0, -1);
|
|
const staticNode = ctx.static[path];
|
|
if (staticNode && staticNode.methods) {
|
|
const staticMatch = staticNode.methods[method] || staticNode.methods[""];
|
|
if (staticMatch !== void 0) return staticMatch[0];
|
|
}
|
|
const segments = splitPath(path);
|
|
const match = _lookupTree(ctx, ctx.root, method, segments, 0)?.[0];
|
|
if (match === void 0) return;
|
|
if (opts?.params === false) return match;
|
|
return {
|
|
data: match.data,
|
|
params: match.paramsMap ? getMatchParams(segments, match.paramsMap) : void 0
|
|
};
|
|
}
|
|
function _lookupTree(ctx, node, method, segments, index) {
|
|
if (index === segments.length) {
|
|
if (node.methods) {
|
|
const match = node.methods[method] || node.methods[""];
|
|
if (match) return match;
|
|
}
|
|
if (node.param && node.param.methods) {
|
|
const match = node.param.methods[method] || node.param.methods[""];
|
|
if (match) {
|
|
const pMap = match[0].paramsMap;
|
|
if (pMap?.[pMap?.length - 1]?.[2]) return match;
|
|
}
|
|
}
|
|
if (node.wildcard && node.wildcard.methods) {
|
|
const match = node.wildcard.methods[method] || node.wildcard.methods[""];
|
|
if (match) {
|
|
const pMap = match[0].paramsMap;
|
|
if (pMap?.[pMap?.length - 1]?.[2]) return match;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
const segment = segments[index];
|
|
if (node.static) {
|
|
const staticChild = node.static[segment];
|
|
if (staticChild) {
|
|
const match = _lookupTree(ctx, staticChild, method, segments, index + 1);
|
|
if (match) return match;
|
|
}
|
|
}
|
|
if (node.param) {
|
|
const match = _lookupTree(ctx, node.param, method, segments, index + 1);
|
|
if (match) {
|
|
if (node.param.hasRegexParam) {
|
|
const exactMatch = match.find((m) => m.paramsRegexp[index]?.test(segment)) || match.find((m) => !m.paramsRegexp[index]);
|
|
return exactMatch ? [exactMatch] : void 0;
|
|
}
|
|
return match;
|
|
}
|
|
}
|
|
if (node.wildcard && node.wildcard.methods) return node.wildcard.methods[method] || node.wildcard.methods[""];
|
|
}
|
|
|
|
/**
|
|
* Remove a route from the router context.
|
|
*/
|
|
function removeRoute(ctx, method, path) {
|
|
const segments = splitPath(path);
|
|
return _remove(ctx.root, method || "", segments, 0);
|
|
}
|
|
function _remove(node, method, segments, index) {
|
|
if (index === segments.length) {
|
|
if (node.methods && method in node.methods) {
|
|
delete node.methods[method];
|
|
if (Object.keys(node.methods).length === 0) node.methods = void 0;
|
|
}
|
|
return;
|
|
}
|
|
const segment = segments[index];
|
|
if (segment === "*") {
|
|
if (node.param) {
|
|
_remove(node.param, method, segments, index + 1);
|
|
if (_isEmptyNode(node.param)) node.param = void 0;
|
|
}
|
|
return;
|
|
}
|
|
if (segment.startsWith("**")) {
|
|
if (node.wildcard) {
|
|
_remove(node.wildcard, method, segments, index + 1);
|
|
if (_isEmptyNode(node.wildcard)) node.wildcard = void 0;
|
|
}
|
|
return;
|
|
}
|
|
const childNode = node.static?.[segment];
|
|
if (childNode) {
|
|
_remove(childNode, method, segments, index + 1);
|
|
if (_isEmptyNode(childNode)) {
|
|
delete node.static[segment];
|
|
if (Object.keys(node.static).length === 0) node.static = void 0;
|
|
}
|
|
}
|
|
}
|
|
function _isEmptyNode(node) {
|
|
return node.methods === void 0 && node.static === void 0 && node.param === void 0 && node.wildcard === void 0;
|
|
}
|
|
|
|
/**
|
|
* Find all route patterns that match the given path.
|
|
*/
|
|
function findAllRoutes(ctx, method = "", path, opts) {
|
|
if (path.charCodeAt(path.length - 1) === 47) path = path.slice(0, -1);
|
|
const segments = splitPath(path);
|
|
const matches = _findAll(ctx, ctx.root, method, segments, 0);
|
|
if (opts?.params === false) return matches;
|
|
return matches.map((m) => {
|
|
return {
|
|
data: m.data,
|
|
params: m.paramsMap ? getMatchParams(segments, m.paramsMap) : void 0
|
|
};
|
|
});
|
|
}
|
|
function _findAll(ctx, node, method, segments, index, matches = []) {
|
|
const segment = segments[index];
|
|
if (node.wildcard && node.wildcard.methods) {
|
|
const match = node.wildcard.methods[method] || node.wildcard.methods[""];
|
|
if (match) matches.push(...match);
|
|
}
|
|
if (node.param) {
|
|
_findAll(ctx, node.param, method, segments, index + 1, matches);
|
|
if (index === segments.length && node.param.methods) {
|
|
const match = node.param.methods[method] || node.param.methods[""];
|
|
if (match) {
|
|
const pMap = match[0].paramsMap;
|
|
if (pMap?.[pMap?.length - 1]?.[2]) matches.push(...match);
|
|
}
|
|
}
|
|
}
|
|
const staticChild = node.static?.[segment];
|
|
if (staticChild) _findAll(ctx, staticChild, method, segments, index + 1, matches);
|
|
if (index === segments.length && node.methods) {
|
|
const match = node.methods[method] || node.methods[""];
|
|
if (match) matches.push(...match);
|
|
}
|
|
return matches;
|
|
}
|
|
|
|
function routeToRegExp(route = "/") {
|
|
const reSegments = [];
|
|
let idCtr = 0;
|
|
for (const segment of route.split("/")) {
|
|
if (!segment) continue;
|
|
if (segment === "*") reSegments.push(`(?<_${idCtr++}>[^/]*)`);
|
|
else if (segment.startsWith("**")) reSegments.push(segment === "**" ? "?(?<_>.*)" : `?(?<${segment.slice(3)}>.+)`);
|
|
else if (segment.includes(":")) reSegments.push(segment.replace(/:(\w+)/g, (_, id) => `(?<${id}>[^/]+)`).replace(/\./g, "\\."));
|
|
else reSegments.push(segment);
|
|
}
|
|
return /* @__PURE__ */ new RegExp(`^/${reSegments.join("/")}/?$`);
|
|
}
|
|
|
|
export { NullProtoObj, addRoute, createRouter, findAllRoutes, findRoute, removeRoute, routeToRegExp }; |