core/router/utils/paths.js

import pathToRegexp from 'path-to-regexp'
import * as R from 'ramda'
import * as str from '@/core/utils/strings'
import { matchRoute, parseUrl } from './parseUrl'

/**
 * @memberof RouterUtils
 * @function mapChildren
 * @description recursively loop over any route children returns a functions that expects an array of routes
 *
 * @property {String} base - The base path
 *
 * @return {Function}
 */
function mapChildren(base) {
	return R.compose(
		R.flatten,
		R.map(({ children, path, ...rest }) => {
			const container = []
			const tmpPath = path ? str.beautifyPath(`${base}${path}`) : ':regex'

			container.push({
				path: tmpPath,
				pattern: typeof path === 'string' ? false : path,
				parent: str.beautifyPath(base),
				...rest
			})

			if (children) {
				const items = Array.isArray(children) ? children : [children]
				container.push(mapChildren(`${base}${path}`)(items))
			}
			return container
		})
	)
}

/**
 * @memberof RouterUtils
 * @function flattenRoutes
 * @description Flatten the nest route map into a 1 dimensional array
 * @property {Array} - The routes array to flatten
 * @return {Array} - The flattened route
 */
export const flattenRoutes = R.reduce((acc, curr) => {
	const { path: tmpPath, children, ...rest } = curr
	const tmp = []

	const path = tmpPath

	tmp.push({ path: str.beautifyPath(tmpPath), ...rest })

	if (children) {
		const items = Array.isArray(children) ? children : [children]
		tmp.push(...mapChildren(path)(items))
	}

	acc.push(...tmp)

	return acc
}, [])

/**
 * @memberof RouterUtils
 * @function matches
 * @description finds all the route matches and gives each match a score
 * @property {Array} routes - The routes array to search through
 * @property {Object} data - data about the requested url
 * @return {Array} - An array of matching routes
 */

const matches = (routes, data) => {
	const { path: slug } = data

	return R.compose(
		R.reverse,
		R.sortBy(R.prop('score')),
		R.map(({ path, pattern, ...rest }) => {
			const last = str.slugFromPath(slug)
			const slugLength = str.segmentize(slug).length
			const pathLength = str.segmentize(path).length
			let score = 1
			let pageNo = null

			// exact match?
			if (str.beautifyPath(slug) === path) {
				score = 5
			} else {
				// pattern, and the last segment matches the regexp
				if (pattern && pathToRegexp(pattern).exec(last)) {
					score = 4
					// get the page number
					pageNo = R.compose(parseInt, R.head, R.match(/\d+/g))(last)
				} else if (slugLength === pathLength) {
					score = 3
				}
			}

			return { route: path, score, pattern, pageNo, params: data, ...rest }
		}),
		R.filter(({ path }) => matchRoute(path)(str.beautifyPath(slug)))
	)(routes)
}

/**
 * @typedef {Object} Route
 * @property {Object} route - the matched route
 * @property {Object} rest - all the other properties from the url data
 * @property {Object} params - any url params
 */

/**
 * @memberof RouterUtils
 * @function findRoute
 * @description function used to match urls to routes
 * @property {Array} routes - The routes array to search through
 * @return {Route}
 */
export const findRoute = routes => {
	const routeMap = flattenRoutes(routes)
	return url => {
		const data = parseUrl(url)
		const { isRoot } = data

		if (isRoot) {
			const { path: route, ...rest } = R.find(R.propEq('path', '/'))(routes)

			return {
				route,
				...rest,
				params: data
			}
		}

		const match = R.head(matches(routeMap, data))

		if (match) {
			return match
		}

		const { path: route, ...rest } = R.find(R.propEq('path', '*'))(routes)

		return {
			route,
			...rest,
			params: data
		}
	}
}

// const find = findRoute(routes)

// find('https://www.our.com/a/p2?test=hello#terry') // ?