import type { RoutePattern } from './route-pattern.ts' import type { Match } from './match/types.ts' import { decodeHostname } from './match/decode.ts' /** * Returns true if match `a` is less specific than match `b`. * * @param a the first match to compare * @param b the second match to compare * @returns true if `a` is less specific than `b` */ export function lessThan(a: Match, b: Match): boolean { return compare(a, b) === -1 } /** * Returns true if match `a` is more specific than match `b`. * * @param a the first match to compare * @param b the second match to compare * @returns true if `a` is more specific than `b` */ export function greaterThan(a: Match, b: Match): boolean { return compare(a, b) === 1 } /** * Returns true if matches `a` and `b` have equal specificity. * * @param a the first match to compare * @param b the second match to compare * @returns true if `a` and `b` have equal specificity */ export function equal(a: Match, b: Match): boolean { return compare(a, b) === 0 } /** * Comparator function for sorting matches from least specific to most specific. * * @param a the first match to compare * @param b the second match to compare * @returns negative if `a` is less specific, positive if more specific, 0 if equal */ export const ascending = (a: Match, b: Match): number => compare(a, b) /** * Comparator function for sorting matches from most specific to least specific. * * @param a the first match to compare * @param b the second match to compare * @returns positive if `a` is less specific, negative if more specific, 0 if equal */ export const descending = (a: Match, b: Match): number => compare(a, b) * -1 /** * Compare two matches by specificity. * Passing to `.sort()` will sort matches from least specific to most specific. * * @param a the first match to compare * @param b the second match to compare * @returns -1 if `a` is less specific, 1 if `a` is more specific, 0 if tied. */ export function compare(a: Match, b: Match): -1 | 0 | 1 { if (a.url.href !== b.url.href) { throw new Error(`Cannot compare matches for different URLs: ${a.url.href} vs ${b.url.href}`) } let hostname = decodeHostname(a.url.hostname) let hostnameResult = compareHostname(hostname, a.paramsMeta.hostname, b.paramsMeta.hostname) if (hostnameResult !== 0) return hostnameResult let pathnameResult = comparePathname(a.paramsMeta.pathname, b.paramsMeta.pathname) if (pathnameResult !== 0) return pathnameResult let searchResult = compareSearch(a.pattern.search, b.pattern.search) if (searchResult !== 0) return searchResult return 0 } function compareHostname( hostname: string, a: Match['paramsMeta']['hostname'], b: Match['paramsMeta']['hostname'], ): -1 | 0 | 1 { if (a.length === 0 && b.length === 0) return 0 if (a.length === 0 && b.length > 0) return 1 if (a.length > 0 && b.length === 0) return -1 // Encoding of hostname chars: 0 = static, 1 = variable (:), 2 = wildcard (*) // Note: `Int8Array` defaults to 0 for all indices not explicitly set. let aEncoding = new Int8Array(hostname.length) for (let range of a) { aEncoding.fill(range.type === ':' ? 1 : 2, range.begin, range.end) } let bEncoding = new Int8Array(hostname.length) for (let range of b) { bEncoding.fill(range.type === ':' ? 1 : 2, range.begin, range.end) } // Build segments right-to-left: desc order by begin let segments: Array<{ begin: number; end: number }> = [] let end = hostname.length for (let i = hostname.length - 1; i >= 0; i--) { if (hostname[i] === '.') { segments.push({ begin: i + 1, end }) end = i } } segments.push({ begin: 0, end }) // leftmost segment for (let segment of segments) { for (let j = segment.begin; j < segment.end; j++) { if (aEncoding[j] < bEncoding[j]) return 1 // a is more specific if (aEncoding[j] > bEncoding[j]) return -1 // b is more specific } } return 0 } function comparePathname( a: Match['paramsMeta']['pathname'], b: Match['paramsMeta']['pathname'], ): -1 | 0 | 1 { if (a.length === 0 && b.length === 0) return 0 if (a.length === 0 && b.length > 0) return 1 if (a.length > 0 && b.length === 0) return -1 let i = 0 let aIndex = 0 let bIndex = 0 while (aIndex < a.length || bIndex < b.length) { let aRange = a[aIndex] let bRange = b[bIndex] if (aRange === undefined) return 1 // a is fully static from here if (bRange === undefined) return -1 // b is fully static from here // Skip to the minimum begin of the two current ranges i = Math.min(aRange.begin, bRange.begin) if (i < aRange.begin) return 1 // a has static content at i if (i < bRange.begin) return -1 // b has static content at i if (aRange.type === ':' && bRange.type === '*') return 1 // a is more specific if (aRange.type === '*' && bRange.type === ':') return -1 // b is more specific let minEnd = Math.min(aRange.end, bRange.end) i = minEnd // Advance range indices if we've reached their ends if (i >= aRange.end) aIndex += 1 if (i >= bRange.end) bIndex += 1 } return 0 } function compareSearch(a: RoutePattern['search'], b: RoutePattern['search']): -1 | 0 | 1 { let aSpecificity = searchSpecificity(a) let bSpecificity = searchSpecificity(b) if (aSpecificity.keyValue > bSpecificity.keyValue) return 1 if (aSpecificity.keyValue < bSpecificity.keyValue) return -1 if (aSpecificity.key > bSpecificity.key) return 1 if (aSpecificity.key < bSpecificity.key) return -1 return 0 } function searchSpecificity(constraints: RoutePattern['search']): { key: number keyValue: number } { let specificity = { key: 0, keyValue: 0 } for (let constraint of constraints.values()) { if (constraint.size === 0) { specificity.key += 1 continue } specificity.keyValue += constraint.size } return specificity }