import type { PartPattern, PartPatternToken, RoutePattern } from '../route-pattern.ts' import { escape } from './regexp.ts' import { unreachable } from '../unreachable.ts' import { toRegExp } from './regexp.ts' export type Variant = { readonly protocol: ProtocolVariant readonly hostname: HostnameVariant readonly port: string readonly pathname: PathnameVariant } export function generateVariants(pattern: RoutePattern): ReadonlyArray { let result: Array = [] for (let protocol of generateProtocolVariants(pattern.protocol)) { let port = normalizePort(protocol, pattern.port) for (let hostname of generateHostnameVariants(pattern.hostname)) { for (let pathname of generatePathnameVariants(pattern.pathname)) { result.push({ protocol, hostname, port, pathname }) } } } return result } // Protocol ---------------------------------------------------------------------------------------- type ProtocolVariant = 'http' | 'https' function generateProtocolVariants( protocol: RoutePattern['protocol'], ): ReadonlyArray { if (protocol === null || protocol === 'http(s)') return ['http', 'https'] return [protocol] } function normalizePort(protocol: ProtocolVariant, port: string | null): string { if (port === null) return '' if (protocol === 'http' && port === '80') return '' if (protocol === 'https' && port === '443') return '' return port } // Hostname ---------------------------------------------------------------------------------------- export type Param = Extract function toParams(tokens: PartVariant): ReadonlyArray { let params: Array = [] for (let token of tokens) { if (token.type === ':' || token.type === '*') { params.push(token) } } return params } type HostnameVariant = | { readonly type: 'static' readonly value: string } | { readonly type: 'dynamic' readonly params: ReadonlyArray readonly regexp: RegExp } | { readonly type: 'any' } function generateHostnameVariants(hostname: PartPattern | null): ReadonlyArray { let result: Array = [] if (hostname === null) return [{ type: 'any' }] for (let variant of generatePartVariants(hostname)) { let params = toParams(variant) if (params.length > 0) { result.push({ type: 'dynamic', params, regexp: toRegExp(variant, { separator: '.', ignoreCase: false }), }) } else { result.push({ type: 'static', value: stringifyStatic(variant, '.') }) } } return result } function stringifyStatic(variant: PartVariant, separator: string): string { let result = '' for (let token of variant) { if (token.type === 'text') result += token.text else if (token.type === 'separator') result += separator else throw new Error(`expected static part variant, got token type '${token.type}'`) } return result } // Pathname ---------------------------------------------------------------------------------------- export type PathnameVariantSegment = | { readonly type: 'static'; readonly key: string } | { readonly type: 'variable' readonly key: string readonly regexp: RegExp readonly params: ReadonlyArray } | { readonly type: 'wildcard' readonly key: string readonly regexp: RegExp readonly params: ReadonlyArray } export type PathnameVariant = ReadonlyArray function generatePathnameVariants( pathname: PartPattern, options?: { ignoreCase?: boolean }, ): ReadonlyArray> { let result: Array> = [] let ignoreCase = options?.ignoreCase ?? false for (let tokens of generatePartVariants(pathname)) { let variant: Array = [] let key = '' let reSource = '' let reFlags = ignoreCase ? 'di' : 'd' let type: 'static' | 'variable' | 'wildcard' = 'static' let params: Array = [] for (let token of tokens) { if (token.type === 'separator') { if (type === 'static') { variant.push({ type: 'static', key: ignoreCase ? key.toLowerCase() : key }) key = '' reSource = '' continue } if (type === 'variable') { variant.push({ type: 'variable', key, regexp: new RegExp(`^${reSource}$`, reFlags), params, }) key = '' reSource = '' params = [] type = 'static' continue } if (type === 'wildcard') { key += '/' reSource += escape('/') continue } unreachable(type) } if (token.type === 'text') { // Encode to comply with URL pathname normalization in trie matcher let text = encodeURIComponent(token.text) key += text reSource += escape(text) continue } if (token.type === ':') { key += '{:}' reSource += `([^/]+)` params.push(token) if (type === 'static') type = 'variable' continue } if (token.type === '*') { key += '{*}' reSource += `(.*)` params.push(token) type = 'wildcard' continue } unreachable(token.type) } if (type === 'static') { variant.push({ type: 'static', key: ignoreCase ? key.toLowerCase() : key }) } if (type === 'variable' || type === 'wildcard') { variant.push({ type, key, regexp: new RegExp(`^${reSource}$`, reFlags), params }) } result.push(variant) } return result } // Part -------------------------------------------------------------------------------------------- type PartVariantToken = Exclude type PartVariant = ReadonlyArray /** * Expand a part pattern's optionals into the list of all concrete variants. * * Each variant is the linear token sequence you'd get by independently choosing * to include or omit every `(` `)` group. No nesting, no optional markers. * * @private */ export function generatePartVariants(part: PartPattern): ReadonlyArray { let result: Array = [] let stack: Array<{ index: number; tokens: Array }> = [{ index: 0, tokens: [] }] while (stack.length > 0) { let { index, tokens } = stack.pop()! if (index === part.tokens.length) { result.push(tokens) continue } let token = part.tokens[index] if (token.type === '(') { stack.push( { index: index + 1, tokens }, { index: part.optionals.get(index)! + 1, tokens: tokens.slice() }, ) continue } if (token.type === ')') { stack.push({ index: index + 1, tokens }) continue } if ( token.type === ':' || token.type === '*' || token.type === 'text' || token.type === 'separator' ) { tokens.push(token) stack.push({ index: index + 1, tokens }) continue } unreachable(token.type) } return result }