import { RoutePattern } from './route-pattern.ts' import type { PartPattern, PartPatternToken } from './route-pattern.ts' import type { JoinPatterns } from './types/join.ts' /** * Join two route patterns. * * Origin parts (`protocol`, `hostname`, `port`) from `next` override `base` when present. * Pathnames are concatenated with a separator inserted between them as needed. * Search constraints from both patterns are merged. * * @param base The base pattern. * @param next The next pattern to join onto `base`. * @returns The joined route pattern. */ export function joinPatterns( base: base | RoutePattern, next: next | RoutePattern, ): RoutePattern> { base = typeof base === 'string' ? RoutePattern.parse(base) : base next = typeof next === 'string' ? RoutePattern.parse(next) : next return new RoutePattern>({ protocol: next.protocol ?? base.protocol, hostname: next.hostname ?? base.hostname, port: next.port ?? base.port, pathname: joinPathname(base.pathname, next.pathname), search: joinSearch(base.search, next.search), }) } /** * Join two pathname parts, inserting a separator between them as needed. * * Trailing separator is stripped from `base`; leading separator is added to `next` if absent. * * ```text * 'a' + 'b' -> 'a/b' * 'a/' + 'b' -> 'a/b' * 'a' + '/b' -> 'a/b' * 'a/' + '/b' -> 'a/b' * '(a)' + '(b)' -> '(a)/(b)' * '(a/)'+ '(b)' -> '(a)/(b)' * '(a)' +'(/b)' -> '(a)(/b)' * '(a/)'+'(/b)' -> '(a)(/b)' * ``` * * @private */ function joinPathname(base: PartPattern, next: PartPattern): PartPattern { if (base.tokens.length === 0) return next if (next.tokens.length === 0) return base let tokens: Array = [] // strip `base`'s trailing separator (only optionals after it) let baseLastNonOptionalIndex = base.tokens.findLastIndex( (token) => token.type !== '(' && token.type !== ')', ) let baseLastNonOptional = base.tokens[baseLastNonOptionalIndex] let baseHasTrailingSeparator = baseLastNonOptional?.type === 'separator' base.tokens.forEach((token, index) => { if (index === baseLastNonOptionalIndex && token.type === 'separator') return tokens.push(token) }) // add separator if `next` has no leading one (only optionals before it) let nextFirstNonOptional = next.tokens.find((token) => token.type !== '(' && token.type !== ')') let needsSeparator = nextFirstNonOptional === undefined || nextFirstNonOptional.type !== 'separator' if (needsSeparator) tokens.push({ type: 'separator' }) let tokenOffset = tokens.length next.tokens.forEach((token) => tokens.push(token)) let optionals = new Map() for (let [begin, end] of base.optionals) { if (baseHasTrailingSeparator) { // one less token before this optional since trailing slash token was omitted if (begin > baseLastNonOptionalIndex) begin -= 1 if (end > baseLastNonOptionalIndex) end -= 1 } optionals.set(begin, end) } for (let [begin, end] of next.optionals) { optionals.set(tokenOffset + begin, tokenOffset + end) } return { tokens, optionals, type: 'pathname' } } /** * Merge two search constraint maps. * * ```text * '?a' + '?b' -> '?a&b' * '?a=1' + '?a=2' -> '?a=1&a=2' * '?a=1' + '?b=2' -> '?a=1&b=2' * '' + '?a' -> '?a' * ``` * * @private */ function joinSearch( base: RoutePattern['search'], next: RoutePattern['search'], ): RoutePattern['search'] { let result = new Map>() for (let [name, values] of base) { result.set(name, new Set(values)) } for (let [name, values] of next) { let current = result.get(name) if (current === undefined) { result.set(name, new Set(values)) continue } for (let value of values) { current.add(value) } } return result }