@contextjs/routing
Version:
Declarative, fast, and extensible route matching for ContextJS applications.
132 lines (131 loc) • 5.15 kB
JavaScript
import { Dictionary } from "@contextjs/collections";
import { ObjectExtensions, StringExtensions } from "@contextjs/system";
import { ParsedRoute } from "../models/parsed-route.js";
import { SegmentKind } from "../models/segment-kind.js";
export class RouteService {
static match(value, routeDefinitions) {
if (StringExtensions.isNullOrWhitespace(value))
return null;
value = this.normalizePath(value);
const segments = this.getSegments(value);
const maxScore = segments.length * 10;
let bestMatch = null;
let bestScore = -1;
for (const routeDefinition of routeDefinitions) {
const score = this.getScore(value, routeDefinition.route.decodedTemplate);
if (score > bestScore) {
bestScore = score;
bestMatch = routeDefinition;
if (score === maxScore)
break;
}
}
return this.parseRoute(value, bestMatch);
}
static getSegments(value) {
return StringExtensions.isNullOrWhitespace(value)
? []
: this.normalizePath(value)
.split("/")
.filter(p => p.length > 0);
}
static parseRoute(value, routeDefinition) {
if (ObjectExtensions.isNullOrUndefined(routeDefinition))
return null;
const parameters = new Dictionary();
const valueParts = this.getSegments(value);
const templateParts = routeDefinition.route.decodedTemplate
.split("/")
.filter(p => p.length > 0);
let i = 0, j = 0;
while (i < templateParts.length) {
const segment = this.parseSegment(templateParts[i]);
const part = j < valueParts.length ? valueParts[j] : null;
if (segment.kind === SegmentKind.CatchAll) {
const rest = valueParts.slice(j).join("/");
if (segment.name)
parameters.set(segment.name, rest);
break;
}
if ((segment.kind === SegmentKind.Parameter || segment.kind === SegmentKind.Optional) && segment.name)
parameters.set(segment.name, part || null);
i++;
j++;
}
return new ParsedRoute(routeDefinition, parameters);
}
static getScore(value, template) {
const valueParts = this.getSegments(value);
const templateParts = template.split("/").filter(part => part.length > 0);
const hasCatchAll = this.hasCatchAll(templateParts);
if (!hasCatchAll && valueParts.length > templateParts.length)
return -1;
let score = 0;
let wildcardScore = 0;
let i = 0;
let j = 0;
while (i < templateParts.length) {
const segment = this.parseSegment(templateParts[i]);
const valueSegment = j < valueParts.length ? valueParts[j] : "";
if (segment.kind === SegmentKind.CatchAll) {
wildcardScore += 5;
break;
}
if (StringExtensions.isNullOrWhitespace(valueSegment)) {
if (segment.kind === SegmentKind.Optional) {
score += 1;
i++;
continue;
}
return -1;
}
if (segment.kind === SegmentKind.Literal && segment.raw === valueSegment)
score += 10;
else if (segment.kind === SegmentKind.Optional || segment.kind === SegmentKind.Parameter)
score += 3;
else
return -1;
i++;
j++;
}
if (j < valueParts.length && !hasCatchAll)
return -1;
return score + wildcardScore;
}
static decode(value) {
try {
if (!value.includes("%"))
return value;
let previous = value;
let current = decodeURIComponent(value);
let iterations = 0;
while (current !== previous && iterations++ < 10) {
previous = current;
current = decodeURIComponent(current);
}
return current.toLowerCase();
}
catch {
return value.toLowerCase();
}
}
static normalizePath(value) {
return this.decode(value)
.replace(/^\/+|\/+$/g, "")
.split('?')[0]
.toLowerCase();
}
static parseSegment(segment) {
if (!segment.startsWith("{") || !segment.endsWith("}"))
return { kind: SegmentKind.Literal, raw: segment };
const inner = segment.slice(1, -1);
if (inner.startsWith("*"))
return { kind: SegmentKind.CatchAll, raw: segment, name: inner.slice(1) };
if (inner.endsWith("?"))
return { kind: SegmentKind.Optional, raw: segment, name: inner.slice(0, -1) };
return { kind: SegmentKind.Parameter, raw: segment, name: inner };
}
static hasCatchAll(templateParts) {
return templateParts.some(t => this.parseSegment(t).kind === SegmentKind.CatchAll);
}
}