class RouterError extends Error { constructor(route1, route2, ...params) { // Pass remaining arguments (including vendor specific ones) to parent constructor super(...params); // Maintains proper stack trace for where our error was thrown (only available on V8) if (Error.captureStackTrace) { Error.captureStackTrace(this, RouterError); } this.name = "RouterError"; this.routeA = route1 this.routeB = route2 } } function Child(split, x, i) { this.path = null this.isParams = split[x].isParams ? split[x].word : null this.isFullParams = split[x].isFullParams ? split[x].word : null this.paramVarName = split[x].paramVarName ?? null this.char = !this.isParams && !this.isFullParams ? split[x].word[i] || '/' : null this.count = 0 this.children = [] } function buildChild(x, i, splitPaths) { let splitPath = splitPaths[0] let letter = new Child(splitPath.split, x, i) let consume = [] if (splitPath.split.length === x + 1 && (splitPath.split[x].isParams || splitPath.split[x].isFullParams || splitPath.split[x].word.length === i + 1)) { letter.path = splitPath.entry letter.count += 1 } else { consume = [splitPath] } for (let y = 1; y < splitPaths.length; y++) { let checkPath = splitPaths[y] if (!checkPath.split[x] || checkPath.split[x].isParams !== splitPath.split[x].isParams || checkPath.split[x].isFullParams !== splitPath.split[x].isFullParams || !checkPath.split[x].isParams && !checkPath.split[x].isFullParams && (checkPath.split[x].word[i] || '/') !== letter.char) break consume.push(checkPath) } letter.count += consume.length if (splitPath.split[x].word.length === i || splitPath.split[x].isParams || splitPath.split[x].isFullParams) { x++ i = -1 } while (consume.length) { letter.children.push(buildChild(x, i + 1, consume)) consume.splice(0, letter.children[letter.children.length - 1].count) } return letter } function buildTree(splitPaths) { let builder = [] while (splitPaths.length) { builder.push(buildChild(0, 0, splitPaths)) splitPaths.splice(0, builder[builder.length - 1].count) } return builder } const regParamPrefix = /^::?/ const regCleanNonAschii = /(?![a-zA-Z_])./g const regCleanRest = /_+/g function splitAndSortPaths(paths, separateStatic = true) { let staticPaths = new Map() let paramsPaths = [] let collator = new Intl.Collator('en', { sensitivity: 'accent' }); paths.forEach(function(entry) { if (entry.path[0] !== '/') throw new RouterError(entry, null, 'Specified route was missing forward slash at start') // Collect static paths separately if (entry.path.indexOf('/:') < 0 && separateStatic) { return staticPaths.set(entry.path, { path: entry, params: {} }) } // Collect params path separately paramsPaths.push({ split: entry.path.slice(1).split(/\//g).map(function(word) { let actualWord = word.replace(regParamPrefix, '') return { word: actualWord, isParams: word[0] === ':' && word[1] !== ':', isFullParams: word[0] === ':' && word[1] === ':', paramVarName: word[0] === ':' ? actualWord.replace(regCleanNonAschii, '_').replace(regCleanRest, '_') : null } }), entry, }) }) paramsPaths.sort(function(aGroup, bGroup) { let length = Math.max(aGroup.split.length, bGroup.split.length) for (let x = 0; x < length; x++) { let a = aGroup.split[x] let b = bGroup.split[x] if (!a) return -1 if (!b) return 1 // Full params go last if (a.isFullParams && b.isFullParams) throw new RouterError(aGroup.entry, bGroup.entry, 'Two full path routes found on same level') if (a.isFullParams) return 1 if (b.isFullParams) return -1 // Params go second last if (a.isParams && !b.isParams) return 1 if (!a.isParams && b.isParams) return -1 // otherwise sort alphabetically if not identical if (a.word !== b.word) return collator.compare(a.word, b.word) } throw new RouterError(aGroup, bGroup, 'Two identical paths were found') }) return { staticPaths, paramsPaths, } } const SlashCode = '/'.charCodeAt(0) function getIndex(offset, additions, params) { return (offset + additions) + (params.length ? ' + ' + params.map(a => `offset${a[1]}`).join(' + ') : '') } function treeIntoCompiledCodeReturnPath(indentString, paths, branch, params) { let pathIndex = paths.indexOf(branch.path) if (pathIndex < 0) { throw new RouterError(branch.path, null, 'InternalError: Specified path was not found in paths') } let output = '\n' + indentString + `return {` output += '\n' + indentString + ` path: paths[${pathIndex}],` if (params.length) { output += '\n' + indentString + ` params: {` for (let param of params) { output += '\n' + indentString + ` ${param[0]}: s${param[1]},` } output += '\n' + indentString + ` },` } else { output += '\n' + indentString + ` params: {},` } output += '\n' + indentString + `}` return output } const spaces = ' ' function treeIntoCompiledCodeBranch(paths, branches, indent = 0, params = []) { let output = '' let indentation = spaces.slice(0, (indent - params.length) * 2) let addEndBracket = true for (let i = 0; i < branches.length; i++) { let branch = branches[i] if (i > 0) { if (!branch.isParams && !branch.isFullParams) { output += ' else ' } else { // output += '} //' output += '\n' + indentation } } if (!branch.isParams && !branch.isFullParams) { output += `if (buf[${getIndex(indent, 0, params)}] === ${branch.char.charCodeAt(0)}) { // ${branch.char}` if (branch.path) { output += '\n' + indentation + ` if (buf.length === ${getIndex(indent, 1, params)}) {` output += treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params) output += '\n' + indentation + ` }` } } else { addEndBracket = false let paramVarName = (params.length + 1) + '_' + branch.paramVarName output += `let s${paramVarName} = str.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, buf.indexOf(${SlashCode}, ${getIndex(indent, 0, params)}) >>> 0`})` output += '\n' + indentation + `let offset${paramVarName} = s${paramVarName}.length` output += '\n' + indentation params.push([branch.isParams || branch.isFullParams, paramVarName]) if (branch.isFullParams) { output += treeIntoCompiledCodeReturnPath(indentation, paths, branch, params) } else if (branch.path) { output += '\n' + indentation + `if (buf.length === ${getIndex(indent, 0, params)}) {` output += treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params) output += '\n' + indentation + `}` } } if (branch.children.length) { if (branch.path) { output += ' else ' } else { output += '\n' + indentation + ' ' } output += treeIntoCompiledCodeBranch(paths, branch.children, indent + 1, params.slice()) } if (addEndBracket) { output += '\n' + indentation + '} ' } } return output } function treeIntoCompiledCodeClosure(paths, tree, staticList) { let output = 'return function RBufferStrSliceClosure(str) {' output += '\n let checkStatic = staticList.get(str)' output += '\n if(checkStatic) {' output += '\n return checkStatic' output += '\n }' output += '\n var buf = Buffer.from(str)' output += '\n ' + treeIntoCompiledCodeBranch(paths, tree, 1, []) output += '\n return null' output += '\n}' return new Function('paths', 'staticList', output)(paths, staticList) } export function compilePaths(paths) { let splitPaths = splitAndSortPaths(paths) let tree = buildTree(splitPaths.paramsPaths.slice()) return treeIntoCompiledCodeClosure(paths, tree, splitPaths.staticPaths) }