import { ElementWithXAttributes } from 'alpinejs' import type { Handler } from './handler' export interface Route { /** * The regex pattern used to match the route. * @internal */ readonly pattern: RegExp /** * The raw route path */ readonly path: string /** * The name of the route */ readonly name: string /** * The template element of the route, set by x-template directive. * @internal */ _template_element?: ElementWithXAttributes match(path: string): undefined | { [key: string]: string } handlers: Handler[] templates: string[] } export interface RouteOptions { handlers?: Route['handlers'] interpolate?: boolean templates?: string[] targetID?: string preload?: boolean name?: string } export type MatchResult = | undefined | { [key: string]: string } /** * Creates a new Route object * @param {string} path - route path pattern * @param {RouteOptions} options - route configuration options * @returns {Route} - a route object */ export const createRoute = ( path: string, { templates = [], handlers = [], name }: RouteOptions = {} ): Route => ({ pattern: parse(path), name: name || path, templates, handlers, path, match(path: string) { const m = this.pattern.exec(path) if (m) { return { ...m.groups } } }, }) /** * @param {string} input The route pattern * @returns {RegExp} The compiled regular expression for the route */ export function parse(input: string): RegExp { // split the input string into segments by '/' const segments = input.split('/').filter(Boolean) // construct the regex pattern from the segments const pattern = segments .map((segment) => { // if the segment does not start with ':', return it as a static // path segment if (!segment.startsWith(':')) return '/' + segment // extract the parameter name, modifier, and extension (if any) from // the segment const [, name, modifier, ext] = segment.match(/^:(\w+)([?+*]?)(?:\.(.+))?$/) || [] // check if the segment is a wildcard or optional const isWildcard = modifier === '*' || modifier === '+' const isOptional = modifier === '?' || modifier === '*' // create the base pattern for matching the segment const basePattern = isWildcard ? (isOptional ? '.*?' : '.+') : '[^/]+?' // construct the named capture group pattern for the segment const namedPattern = `(?<${name}>${basePattern})` // if the segment has an extension, add it to the pattern const extensionPattern = ext ? `\\.${ext}` : '' // combine the named pattern and extension pattern const segmentPattern = namedPattern + extensionPattern // if the segment is optional, wrap it in a non-capturing group // with a '?' quantifier if (isOptional) return `(?:/${segmentPattern})?` // return the segment pattern as a required part of the path return `/${segmentPattern}` }) .join('') // create a regex pattern that matches the entire path, allowing for an // optional trailing slash and case insensitivity return new RegExp(`^${pattern}/?$`, 'i') } export default createRoute