UNPKG

@contextjs/routing

Version:

Declarative, fast, and extensible route matching for ContextJS applications.

132 lines (131 loc) 5.15 kB
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); } }