import type { Router, Result, Params } from '../../router.ts' import { METHOD_NAME_ALL, UnsupportedPathError } from '../../router.ts' type RegExpMatchArrayWithIndices = RegExpMatchArray & { indices: [number, number][] } const emptyParams = {} const splitPathRe = /\/(:\w+(?:{[^}]+})?)|\/[^\/\?]+|(\?)/g const splitByStarRe = /\*/ export class LinearRouter implements Router { name: string = 'LinearRouter' routes: [string, string, T][] = [] add(method: string, path: string, handler: T) { if (path.charCodeAt(path.length - 1) === 63) { // /path/to/:label? means /path/to/:label or /path/to this.routes.push([method, path.slice(0, -1), handler]) this.routes.push([method, path.replace(/\/[^/]+$/, ''), handler]) } else { this.routes.push([method, path, handler]) } } match(method: string, path: string): Result { const handlers: [T, Params][] = [] ROUTES_LOOP: for (let i = 0; i < this.routes.length; i++) { const [routeMethod, routePath, handler] = this.routes[i] if (routeMethod !== method && routeMethod !== METHOD_NAME_ALL) { continue } if (routePath === '*' || routePath === '/*') { handlers.push([handler, emptyParams]) continue } const hasStar = routePath.indexOf('*') !== -1 const hasLabel = routePath.indexOf(':') !== -1 if (!hasStar && !hasLabel) { if (routePath === path || routePath + '/' === path) { handlers.push([handler, emptyParams]) } } else if (hasStar && !hasLabel) { const endsWithStar = routePath.charCodeAt(routePath.length - 1) === 42 const parts = (endsWithStar ? routePath.slice(0, -2) : routePath).split(splitByStarRe) const lastIndex = parts.length - 1 for (let j = 0, pos = 0; j < parts.length; j++) { const part = parts[j] const index = path.indexOf(part, pos) if (index !== pos) { continue ROUTES_LOOP } pos += part.length if (j === lastIndex) { if ( !endsWithStar && pos !== path.length && !(pos === path.length - 1 && path.charCodeAt(pos) === 47) ) { continue ROUTES_LOOP } } else { const index = path.indexOf('/', pos) if (index === -1) { continue ROUTES_LOOP } pos = index } } handlers.push([handler, emptyParams]) } else if (hasLabel && !hasStar) { const params: Record = {} const parts = routePath.match(splitPathRe) as string[] const lastIndex = parts.length - 1 for (let j = 0, pos = 0; j < parts.length; j++) { if (pos === -1 || pos >= path.length) { continue ROUTES_LOOP } const part = parts[j] if (part.charCodeAt(1) === 58) { // /:label let name = part.slice(2) let value if (name.charCodeAt(name.length - 1) === 125) { // :label{pattern} const openBracePos = name.indexOf('{') const pattern = name.slice(openBracePos + 1, -1) const restPath = path.slice(pos + 1) const match = new RegExp(pattern, 'd').exec(restPath) as RegExpMatchArrayWithIndices if (!match || match.indices[0][0] !== 0 || match.indices[0][1] === 0) { continue ROUTES_LOOP } name = name.slice(0, openBracePos) value = restPath.slice(...match.indices[0]) pos += match.indices[0][1] + 1 } else { let endValuePos = path.indexOf('/', pos + 1) if (endValuePos === -1) { if (pos + 1 === path.length) { continue ROUTES_LOOP } endValuePos = path.length } value = path.slice(pos + 1, endValuePos) pos = endValuePos } params[name] ||= value as string } else { const index = path.indexOf(part, pos) if (index !== pos) { continue ROUTES_LOOP } pos += part.length } if (j === lastIndex) { if (pos !== path.length && !(pos === path.length - 1 && path.charCodeAt(pos) === 47)) { continue ROUTES_LOOP } } } handlers.push([handler, params]) } else if (hasLabel && hasStar) { throw new UnsupportedPathError() } } return [handlers] } }