diff --git a/.gitignore b/.gitignore index ca8527b..2ebcc74 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.idea + # Logs logs *.log diff --git a/__tests__/tree-spec.js b/__tests__/tree-spec.js index 85e8ece..db9ad06 100644 --- a/__tests__/tree-spec.js +++ b/__tests__/tree-spec.js @@ -105,91 +105,162 @@ describe("Add and get", () => { describe("Wildcard", () => { const tree = new Tree(); const routes = [ - "/", - "/cmd/:tool/:sub", - "/cmd/:tool/", - "/src/*filepath", - "/search/", - "/search/:query", - "/user_:name", - "/user_:name/about", - "/files/:dir/*filepath", - "/doc/", - "/doc/node_faq.html", - "/doc/node1.html", - "/info/:user/public", - "/info/:user/project/:project" - ]; - - routes.forEach(route => { - tree.addRoute(route, noOp); - }); - - // tree.printTree(); - - const foundData = [ { route: "/", - params: [] + found: [ + { + route: "/", + params: [] + }, + ], }, { - route: "/cmd/test/", - params: [{ key: "tool", value: "test" }] + route: "/cmd/foo/bar", + found: [ + { + route: "/cmd/foo/bar", + params: [] + }, + ], }, { - route: "/cmd/test/3", - params: [{ key: "tool", value: "test" }, { key: "sub", value: "3" }] + route: "/cmd/a_:tool", + found: [ + { + route: "/cmd/a_test", + params: [{ key: "tool", value: "test" }] + }, + ], }, { - route: "/src/", - params: [{ key: "filepath", value: "/" }] + route: "/cmd/:tool/", + found: [ + { + route: "/cmd/test/", + params: [{ key: "tool", value: "test" }] + }, + ], }, { - route: "/src/some/file.png", - params: [{ key: "filepath", value: "/some/file.png" }] + route: "/cmd/:tool/:sub", + found: [ + { + route: "/cmd/test/3", + params: [{ key: "tool", value: "test" }, { key: "sub", value: "3" }] + }, + ], + }, + { + route: "/src/*filepath", + found: [ + { + route: "/src/", + params: [{ key: "filepath", value: "/" }] + }, + { + route: "/src/some/file.png", + params: [{ key: "filepath", value: "/some/file.png" }] + }, + ], }, { route: "/search/", - params: [] + found: [ + { + route: "/search/", + params: [] + }, + ], }, { - route: "/search/中文", - params: [{ key: "query", value: "中文" }] + route: "/search/:query", + found: [ + { + route: "/search/中文", + params: [{ key: "query", value: "中文" }] + }, + ], }, { - route: "/user_noder", - params: [{ key: "name", value: "noder" }] + route: "/user_:name", + found: [ + { + route: "/user_noder", + params: [{ key: "name", value: "noder" }] + }, + ], }, { - route: "/user_noder/about", - params: [{ key: "name", value: "noder" }] + route: "/user_:name/about", + found: [ + { + route: "/user_noder/about", + params: [{ key: "name", value: "noder" }] + }, + ], }, { - route: "/files/js/inc/framework.js", - params: [ - { key: "dir", value: "js" }, - { key: "filepath", value: "/inc/framework.js" } - ] + route: "/files/:dir/*filepath", + found: [ + { + route: "/files/js/inc/framework.js", + params: [ + { key: "dir", value: "js" }, + { key: "filepath", value: "/inc/framework.js" } + ] + }, + ], }, { - route: "/info/gordon/public", - params: [{ key: "user", value: "gordon" }] + route: "/doc/", + found: [], }, { - route: "/info/gordon/project/node", - params: [ - { key: "user", value: "gordon" }, - { key: "project", value: "node" } - ] - } + route: "/doc/node_faq.html", + found: [], + }, + { + route: "/doc/node1.html", + found: [], + }, + { + route: "/info/:user/public", + found: [ + { + route: "/info/gordon/public", + params: [{ key: "user", value: "gordon" }] + }, + ], + }, + { + route: "/info/:user/project/:project", + found: [ + { + route: "/info/gordon/project/node", + params: [ + { key: "user", value: "gordon" }, + { key: "project", value: "node" } + ] + }, + ], + }, ]; - foundData.forEach(data => { - it(data.route, () => { - const { handle, params } = tree.search(data.route); - expect(handle).toBeTruthy(); - expect(params).toMatchObject(data.params); - }); + routes.forEach((route) => { + route.handle = noOp.slice(0) // creating an unique handle + tree.addRoute(route.route, route.handle); + }); + + // tree.printTree(); + + routes.forEach((route) => { + route.found.forEach((data) => { + it(data.route, () => { + const { handle, params } = tree.search(data.route); + expect(handle).toBe(route.handle); + expect(params).toMatchObject(data.params); + }); + }) }); const noHandlerData = [ diff --git a/tree.js b/tree.js index a7ef623..653c16c 100644 --- a/tree.js +++ b/tree.js @@ -81,6 +81,7 @@ class Node { ) { this.path = path; this.wildChild = wildChild; + this.wildChildIdx = -1; this.type = type; this.indices = indices; this.children = children; @@ -139,7 +140,7 @@ class Node { let i = longestCommonPrefix(path, n.path); // Split edge - if (i < n.path.length) { + if (i && i < n.path.length) { const child = new Node( n.path.slice(i), n.wildChild, @@ -162,16 +163,18 @@ class Node { path = path.slice(i); if (n.wildChild) { - n = n.children[0]; + n = n.children[n.wildChildIdx]; n.priority++; // Check if the wildcard matches if ( - path.length >= n.path.length && - n.path === path.slice(0, n.path.length) && // Adding a child to a catchAll is not possible n.type !== CATCH_ALL && - (n.path.length >= path.length || path[n.path.length] === "/") + path.length >= n.path.length && + // exactly matches wildcard and ...v + n.path === path.slice(0, n.path.length) && + // either exact match or starts a new subpath by slash + (n.path.length === path.length || path[n.path.length] === "/") ) { continue walk; } else { @@ -261,16 +264,6 @@ class Node { ); } - if (n.children.length > 0) { - throw new Error( - "wildcard route '" + - wildcard + - "' conflicts with existing children in path '" + - fullPath + - "'" - ); - } - if (wildcard[0] === ":") { // param if (i > 0) { @@ -280,8 +273,9 @@ class Node { } n.wildChild = true; + n.wildChildIdx = n.children.length; const child = new Node(wildcard, false, PARAM); - n.children = [child]; + n.children.push(child); n = child; n.priority++; @@ -355,11 +349,21 @@ class Node { * @param {string} path */ search(path) { + let bk = null; let handle = null; - const params = []; + let params = []; let n = this; + let skip = 0; + let reset = false; walk: while (true) { + if (reset) { + ({path, n, params, handle, skip} = bk.pop()); + reset = false; + } + + const wpath = path + if (path.length > n.path.length) { if (path.slice(0, n.path.length) === n.path) { path = path.slice(n.path.length); @@ -376,11 +380,25 @@ class Node { } // Nothing found. + if (bk && bk.length) { + reset = true; + continue walk; + } return { handle, params }; } + if (skip < n.wildChildIdx) { + // has more children to lookup in case nothing will be found + if (!bk) { + bk = [] + } + bk.push({ path: wpath, n, params: params.slice(0), handle, skip: skip + 1 }) + } + + n = n.children[skip]; + skip = 0; + // Handle wildcard child - n = n.children[0]; switch (n.type) { case PARAM: // Find param end @@ -406,6 +424,11 @@ class Node { handle = n.handle; + if (!handle && bk && bk.length) { + reset = true; + continue walk; + } + return { handle, params }; case CATCH_ALL: @@ -414,6 +437,9 @@ class Node { handle = n.handle; return { handle, params }; + case STATIC: + continue walk; + default: throw new Error("invalid node type"); } @@ -422,6 +448,11 @@ class Node { handle = n.handle; } + if (!handle && bk && bk.length) { + reset = true; + continue walk; + } + return { handle, params }; } }