// Ported from nanorouter 4.0.0 and wayfarer 7.0.1 with its trie (MIT) // https://github.com/choojs/nanorouter — https://github.com/yoshuawuyts/wayfarer // Consolidated into a single module: trie-based router with :params and // * wildcards, plus nanorouter's URL normalization on top. import { ok, equal, notEqual } from './assert.js' function has (object, property) { return Object.prototype.hasOwnProperty.call(object, property) } class Trie { constructor () { this.trie = { nodes: {} } } // create a node on the trie at route and return it create (route) { equal(typeof route, 'string', 'route should be a string') // strip leading '/' and split routes const routes = route.replace(/^\//, '').split('/') const self = this function createNode (index, trie) { const thisRoute = has(routes, index) && routes[index] if (thisRoute === false) return trie let node = null if (/^:|^\*/.test(thisRoute)) { // if node is a name match, set name and append to ':' node if (!has(trie.nodes, '$$')) { node = { nodes: {} } trie.nodes.$$ = node } else { node = trie.nodes.$$ } if (thisRoute[0] === '*') { trie.wildcard = true } trie.name = thisRoute.replace(/^:|^\*/, '') } else if (!has(trie.nodes, thisRoute)) { node = { nodes: {} } trie.nodes[thisRoute] = node } else { node = trie.nodes[thisRoute] } return createNode(index + 1, node) } return createNode(0, self.trie) } // match a route on the trie and return the node match (route) { equal(typeof route, 'string', 'route should be a string') const routes = route.replace(/^\//, '').split('/') const params = {} function search (index, trie) { // either there's no match, or we're done searching if (trie === undefined) return undefined const thisRoute = routes[index] if (thisRoute === undefined) return trie if (has(trie.nodes, thisRoute)) { // match regular routes first return search(index + 1, trie.nodes[thisRoute]) } else if (trie.name) { // match named routes try { params[trie.name] = decodeURIComponent(thisRoute) } catch (e) { return search(index, undefined) } return search(index + 1, trie.nodes.$$) } else if (trie.wildcard) { // match wildcards try { params.wildcard = decodeURIComponent(routes.slice(index).join('/')) } catch (e) { return search(index, undefined) } // return early, or else search may keep recursing through the wildcard return trie.nodes.$$ } else { // no matches found return search(index + 1) } } let node = search(0, this.trie) if (!node) return undefined node = Object.assign({}, node) node.params = params return node } // mount a trie onto a node at route mount (route, trie) { equal(typeof route, 'string', 'route should be a string') equal(typeof trie, 'object', 'trie should be a object') const split = route.replace(/^\//, '').split('/') let node = null if (split.length === 1) { node = this.create(split[0]) } else { node = this.create(split.join('/')) } Object.assign(node.nodes, trie.nodes) if (trie.name) node.name = trie.name // delegate properties from '/' to the new node // '/' cannot be reached once mounted if (node.nodes['']) { Object.keys(node.nodes['']).forEach(function (key) { if (key === 'nodes') return node[key] = node.nodes[''][key] }) Object.assign(node.nodes, node.nodes[''].nodes) delete node.nodes[''].nodes } } } function wayfarer (dft) { const _default = (dft || '').replace(/^\//, '') const _trie = new Trie() emit._trie = _trie emit.on = on emit.emit = emit emit.match = match emit._wayfarer = true return emit function on (route, cb) { equal(typeof route, 'string') equal(typeof cb, 'function') route = route || '/' if (cb._wayfarer && cb._trie) { _trie.mount(route, cb._trie.trie) } else { const node = _trie.create(route) node.cb = cb node.route = route } return emit } function emit (route, ...args) { const matched = match(route) return matched.cb(matched.params, ...args) } function match (route) { notEqual(route, undefined, "'route' must be defined") const matched = _trie.match(route) if (matched && matched.cb) return new Route(matched) const dft = _trie.match(_default) if (dft && dft.cb) return new Route(dft) throw new Error("route '" + route + "' did not match") } function Route (matched) { this.cb = matched.cb this.route = matched.route this.params = matched.params } } // electron support const isLocalFile = /file:\/\//.test( typeof window === 'object' && window.location && window.location.origin ) /* eslint-disable no-useless-escape */ const electron = '^(file:\/\/|\/)(.*\.html?\/?)?' const protocol = '^(http(s)?(:\/\/))?(www\.)?' const domain = '[a-zA-Z0-9-_\.]+(:[0-9]{1,5})?(\/{1})?' const qs = '[\?].*$' /* eslint-enable no-useless-escape */ const stripElectron = new RegExp(electron) const prefix = new RegExp(protocol + domain) const normalize = new RegExp('#') const suffix = new RegExp(qs) // replace everything in a route but the pathname and hash function pathname (routename, isElectron) { if (isElectron) routename = routename.replace(stripElectron, '') else routename = routename.replace(prefix, '') return decodeURI(routename.replace(suffix, '').replace(normalize, '/')) } export default class Nanorouter { constructor (opts) { opts = opts || {} this.router = wayfarer(opts.default || '/404') } on (routename, listener) { equal(typeof routename, 'string') routename = routename.replace(/^[#/]/, '') this.router.on(routename, listener) } emit (routename) { equal(typeof routename, 'string') routename = pathname(routename, isLocalFile) return this.router.emit(routename) } match (routename) { equal(typeof routename, 'string') routename = pathname(routename, isLocalFile) return this.router.match(routename) } } export { Trie, wayfarer, ok }