UNPKG

sensai

Version:

Because even AI needs a master

243 lines (242 loc) 8.68 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "default", { enumerable: true, get: function() { return _default; } }); const _constants = require("../../constants"); const _default = (tree = createNode())=>{ return { /** * Add tree branch. * A branch is made of segments that are either static or dynamic. The last * segment is marked as the end of a branch. * A dynamic segment can be: * - `parametric` (single segment value swapping) * - `catch-all` (multiple segments value swapping) * - `optional catch-all` (zero or multiple segments value swapping) * * @examples * * add('/') * add('/hello) * add('/hello/[name]) * add('/hello/[...names]) * // optional catch-all * add('/hello/[[...names]]) */ add (folderPath) { let current = tree; for (const segment of folderPath.split("/")){ if (segment) { let child = current.segments[segment]; if (!child) { child = current.segments[segment] = createNode(); // add metadata for new nodes const dynamicSegment = getDynamic(segment); if (dynamicSegment) { current.metadata = prioritize(current.metadata, dynamicSegment); } } current = child; } } current.end = true; return true; }, /** * This method is used for test purpose so we could check if a node * was created in the tree with the right attributes (i.e `end`, `metadata` and `segments`). * @private */ get (folderPath) { let current = tree; for (const segment of folderPath.split("/")){ if (segment) { let child = current.segments[segment]; if (!child) return; current = child; } } return current; }, /** * Get radix-like tree. */ getTree () { return tree; }, /** * Lookup a path in the tree. * Path segments are compared to segment in the tree and values are swapped * if needed (see examples). * * @examples * // static * add('/hello') * lookup('/hello') * // => { path: '/hello', params: {}} * * // parametric * add('/hello/[name]') * lookup('/hello/world') * // => { path: '/hello/[name]', params: { name: 'world' }} * * // catch-all * add('/hello/[...names]') * lookup('/hello/john/jane') * // => { path: '/hello/[...names]', params: { names: ['john', 'jane'] }} * add('/hello/[name]') * lookup('/hello/john/jane') * // => undefined // TODO we could return /hello/[...names] * lookup('/hello/john') * // => { path: '/hello/[name]', params: { name: 'john' }} * * // optional catch-all * add('/hello/[[...names]]') * lookup('/hello/john/jane') * // => { path: '/hello/[...names]', params: { names: ['john', 'jane'] }} * lookup('/hello') * // => { path: '/hello/[...names]', params: { names: [] }} * // static segments are more specific * add('/hello') * lookup('/hello') * // => { path: '/hello/', params: {}} */ lookup (urlPath) { let current = tree; const params = {}; let path = ""; const segments = urlPath.split("/"); for(let i = 0, l = segments.length; i < l; i++){ const segment = segments[i]; if (segment) { let child = current.segments[segment]; // Check if there's a static match first if (child && child.end) { current = child; path += "/" + segment; continue; } // If static node exists but it's not a route endpoint (end=false), // or if no static node exists, check for dynamic segments const dynamicSegment = current.metadata[0]; if (dynamicSegment) { const { name, key, level } = dynamicSegment; const dynamicNode = current.segments[name]; if (!dynamicNode) return; // Dynamic segment doesn't exist current = dynamicNode; path += "/" + name; if (level === 1) { params[key] = segment; continue; } else { params[key] = segments.slice(i).filter(Boolean); // TODO we assume url is not sanitized, should we? break; } } else if (child) { // There's a static match but it's not an endpoint current = child; path += "/" + segment; } else { // No match at all return; } } } if (current.end) { return { path: path || "/", params }; } else { const dynamicSegment = current.metadata[0]; if (dynamicSegment) { const { name, key, level } = dynamicSegment; if (level === 3) { // if optional const dynamicNode = current.segments[name]; // if optional was removed or doesn't exist if (!dynamicNode || !dynamicNode.end) return undefined; return { path: path + "/" + name, params: { ...params, [key]: [] } }; } } } }, /** * Remove branch from the tree. * The entire algorithm is based on file-system and while we can remove a branch, it does * not mean a folder is removed (the `isRecursive` argument allows to delete nodes from a branch). */ remove (folderPath, isRecursive = false) { let current = tree; var segment = ""; let parent; for (segment of folderPath.split("/")){ if (segment) { parent = current; let child = current.segments[segment]; if (!child) return false; current = child; } } current.end = false; if (isRecursive) { // remove child segment and metadata if exists if (parent && segment) { delete parent.segments[segment]; parent.metadata = parent.metadata.filter((dynamic)=>dynamic.name !== segment); } } return true; } }; }; /** * Create tree node. * The `metadata` attribute contains information about dynamic children nodes. */ const createNode = ()=>{ return { end: false, metadata: [], segments: {} }; }; /** * Get dynamic segment metadata. */ const getDynamic = (segment)=>{ const matches = segment.match(_constants.DYNAMIC_SEGMENT_REGEXP); if (matches) { const isCatchAll = !!matches[2]; let level = 1; if (isCatchAll) { level = 2; if (matches[1] === "[[" && isCatchAll) level = 3; } return { key: matches[3], name: segment, level }; } }; /** * Add new dynamic parameters to a stack and sort it by priority * and name (for parameters of same level). */ const prioritize = (stack, dynamic)=>{ stack.splice(0, 0, dynamic); return stack.sort((a, b)=>{ if (a.level === b.level) { // when levels are the same, sort by name return a.name.localeCompare(b.name); } else { // otherwise, sort by level return a.level - b.level; } }); };