import { escapeStringRegexp } from '../../escape-regexp' interface Group { pos: number repeat: boolean optional: boolean } function parseParameter(param: string) { const optional = param.startsWith('[') && param.endsWith(']') if (optional) { param = param.slice(1, -1) } const repeat = param.startsWith('...') if (repeat) { param = param.slice(3) } return { key: param, repeat, optional } } export function getParametrizedRoute(route: string) { const segments = (route.replace(/\/$/, '') || '/').slice(1).split('/') const groups: { [groupName: string]: Group } = {} let groupIndex = 1 const parameterizedRoute = segments .map((segment) => { if (segment.startsWith('[') && segment.endsWith(']')) { const { key, optional, repeat } = parseParameter(segment.slice(1, -1)) groups[key] = { pos: groupIndex++, repeat, optional } return repeat ? (optional ? '(?:/(.+?))?' : '/(.+?)') : '/([^/]+?)' } else { return `/${escapeStringRegexp(segment)}` } }) .join('') // dead code eliminate for browser since it's only needed // while generating routes-manifest if (typeof window === 'undefined') { let routeKeyCharCode = 97 let routeKeyCharLength = 1 // builds a minimal routeKey using only a-z and minimal number of characters const getSafeRouteKey = () => { let routeKey = '' for (let i = 0; i < routeKeyCharLength; i++) { routeKey += String.fromCharCode(routeKeyCharCode) routeKeyCharCode++ if (routeKeyCharCode > 122) { routeKeyCharLength++ routeKeyCharCode = 97 } } return routeKey } const routeKeys: { [named: string]: string } = {} let namedParameterizedRoute = segments .map((segment) => { if (segment.startsWith('[') && segment.endsWith(']')) { const { key, optional, repeat } = parseParameter(segment.slice(1, -1)) // replace any non-word characters since they can break // the named regex let cleanedKey = key.replace(/\W/g, '') let invalidKey = false // check if the key is still invalid and fallback to using a known // safe key if (cleanedKey.length === 0 || cleanedKey.length > 30) { invalidKey = true } if (!isNaN(parseInt(cleanedKey.slice(0, 1)))) { invalidKey = true } if (invalidKey) { cleanedKey = getSafeRouteKey() } routeKeys[cleanedKey] = key return repeat ? optional ? `(?:/(?<${cleanedKey}>.+?))?` : `/(?<${cleanedKey}>.+?)` : `/(?<${cleanedKey}>[^/]+?)` } else { return `/${escapeStringRegexp(segment)}` } }) .join('') return { parameterizedRoute, namedParameterizedRoute, groups, routeKeys, } } return { parameterizedRoute, groups, } } export interface RouteRegex { groups: { [groupName: string]: Group } namedRegex?: string re: RegExp routeKeys?: { [named: string]: string } } export function getRouteRegex(normalizedRoute: string): RouteRegex { const result = getParametrizedRoute(normalizedRoute) if ('routeKeys' in result) { return { re: new RegExp(`^${result.parameterizedRoute}(?:/)?$`), groups: result.groups, routeKeys: result.routeKeys, namedRegex: `^${result.namedParameterizedRoute}(?:/)?$`, } } return { re: new RegExp(`^${result.parameterizedRoute}(?:/)?$`), groups: result.groups, } }