UNPKG

route-trie

Version:

A minimal and powerful trie based url path router for Node.js.

320 lines 11.5 kB
// **Github:** https://github.com/zensh/route-trie // // **License:** MIT 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); // the valid characters for the path component: // [A-Za-z0-9!$%&'()*+,-.:;=@_~] // http://stackoverflow.com/questions/4669692/valid-characters-for-directory-part-of-a-url-for-short-links // https://tools.ietf.org/html/rfc3986#section-3.3 const wordReg = /^\w+$/; const suffixReg = /\+[A-Za-z0-9!$%&'*+,-.:;=@_~]*$/; const doubleColonReg = /^::[A-Za-z0-9!$%&'*+,-.:;=@_~]*$/; const trimSlashReg = /^\//; const fixMultiSlashReg = /\/{2,}/g; class Matched { constructor() { // Either a Node pointer when matched or nil this.node = null; this.params = {}; // If FixedPathRedirect enabled, it may returns a redirect path, // otherwise a empty string. this.fpr = ''; // If TrailingSlashRedirect enabled, it may returns a redirect path, // otherwise a empty string. this.tsr = ''; } } exports.Matched = Matched; class Node { constructor(parent) { this.name = ''; this.allow = ''; this.pattern = ''; this.segment = ''; this.suffix = ''; this.regex = null; this.endpoint = false; this.wildcard = false; this.varyChildren = []; this.parent = parent; this.children = Object.create(null); this.handlers = Object.create(null); } handle(method, handler) { if (handler == null) { throw new TypeError('handler should not be null'); } if (this.handlers[method] != null) { throw new Error(`"${method}" already defined`); } this.handlers[method] = handler; if (this.allow === '') { this.allow = method; } else { this.allow += ', ' + method; } } getHandler(method) { return this.handlers[method] == null ? null : this.handlers[method]; } getAllow() { return this.allow; } getPattern() { return this.pattern; } getSegments() { let segments = this.segment; if (this.parent != null) { segments = this.parent.getSegments() + '/' + segments; } return segments; } } exports.Node = Node; class Trie { constructor(options = {}) { // Ignore case when matching URL path. this.ignoreCase = options.ignoreCase !== false; // If enabled, the trie will detect if the current path can't be matched but // a handler for the fixed path exists. // matched.fpr will returns either a fixed redirect path or an empty string. // For example when "/api/foo" defined and matching "/api//foo", // The result matched.fpr is "/api/foo". this.fpr = options.fixedPathRedirect !== false; // If enabled, the trie will detect if the current path can't be matched but // a handler for the path with (without) the trailing slash exists. // matched.tsr will returns either a redirect path or an empty string. // For example if /foo/ is requested but a route only exists for /foo, the // client is redirected to /foo. // For example when "/api/foo" defined and matching "/api/foo/", // The result matched.tsr is "/api/foo". this.tsr = options.trailingSlashRedirect !== false; this.root = new Node(null); } define(pattern) { if (typeof pattern !== 'string') { throw new TypeError('Pattern must be string.'); } if (pattern.includes('//')) { throw new Error('Multi-slash existhis.'); } const _pattern = pattern.replace(trimSlashReg, ''); const node = defineNode(this.root, _pattern.split('/'), this.ignoreCase); if (node.pattern === '') { node.pattern = pattern; } return node; } match(path) { // the path should be normalized before match, just as path.normalize do in Node.js if (typeof path !== 'string') { throw new TypeError('Path must be string.'); } if (path === '' || path[0] !== '/') { throw new Error(`Path is not start with "/": "${path}"`); } let fixedLen = path.length; if (this.fpr) { path = path.replace(fixMultiSlashReg, '/'); fixedLen -= path.length; } let start = 1; let parent = this.root; const end = path.length; const matched = new Matched(); for (let i = 1; i <= end; i++) { if (i < end && path[i] !== '/') { continue; } let segment = path.slice(start, i); let node = matchNode(parent, segment); if (this.ignoreCase && node == null) { node = matchNode(parent, segment.toLowerCase()); } if (node == null) { // TrailingSlashRedirect: /acb/efg/ -> /acb/efg if (this.tsr && segment === '' && i === end && parent.endpoint) { matched.tsr = path.slice(0, end - 1); if (this.fpr && fixedLen > 0) { matched.fpr = matched.tsr; matched.tsr = ''; } } return matched; } parent = node; if (parent.name !== '') { if (parent.wildcard) { matched.params[parent.name] = path.slice(start, end); break; } else { if (parent.suffix !== '') { segment = segment.slice(0, segment.length - parent.suffix.length); } matched.params[parent.name] = segment; } } start = i + 1; } if (parent.endpoint) { matched.node = parent; if (this.fpr && fixedLen > 0) { matched.fpr = path; matched.node = null; } } else if (this.tsr && parent.children[''] != null) { // TrailingSlashRedirect: /acb/efg -> /acb/efg/ matched.tsr = path + '/'; if (this.fpr && fixedLen > 0) { matched.fpr = matched.tsr; matched.tsr = ''; } } return matched; } } Trie.NAME = 'Trie'; Trie.VERSION = 'v3.0.0'; Trie.Node = Node; Trie.Matched = Matched; exports.Trie = Trie; function defineNode(parent, segments, ignoreCase) { const segment = segments.shift(); const child = parseNode(parent, segment, ignoreCase); if (segments.length === 0) { child.endpoint = true; return child; } if (child.wildcard) { throw new Error(`Can not define pattern after wildcard: "${child.pattern}"`); } return defineNode(child, segments, ignoreCase); } function matchNode(parent, segment) { if (parent.children[segment] != null) { return parent.children[segment]; } for (const child of parent.varyChildren) { let _segment = segment; if (child.suffix !== '') { if (segment === child.suffix || !segment.endsWith(child.suffix)) { continue; } _segment = segment.slice(0, segment.length - child.suffix.length); } if (child.regex != null && !child.regex.test(_segment)) { continue; } return child; } return null; } function parseNode(parent, segment, ignoreCase) { let _segment = segment; if (doubleColonReg.test(segment)) { _segment = segment.slice(1); } if (ignoreCase) { _segment = _segment.toLowerCase(); } if (parent.children[_segment] != null) { return parent.children[_segment]; } const node = new Node(parent); if (segment === '') { parent.children[''] = node; } else if (doubleColonReg.test(segment)) { // pattern "/a/::" should match "/a/:" // pattern "/a/::bc" should match "/a/:bc" // pattern "/a/::/bc" should match "/a/:/bc" parent.children[_segment] = node; } else if (segment[0] === ':') { let name = segment.slice(1); switch (name[name.length - 1]) { case '*': name = name.slice(0, name.length - 1); node.wildcard = true; break; default: const n = name.search(suffixReg); if (n >= 0) { node.suffix = name.slice(n + 1); name = name.slice(0, n); if (node.suffix === '') { throw new Error(`invalid pattern: "${node.getSegments()}"`); } } if (name[name.length - 1] === ')') { const i = name.indexOf('('); if (i > 0) { const regex = name.slice(i + 1, name.length - 1); if (regex.length > 0) { name = name.slice(0, i); node.regex = new RegExp(regex); } else { throw new Error(`Invalid pattern: "${node.getSegments()}"`); } } } } // name must be word characters `[0-9A-Za-z_]` if (!wordReg.test(name)) { throw new Error(`Invalid pattern: "${node.getSegments()}"`); } node.name = name; for (const child of parent.varyChildren) { if (child.wildcard) { if (!node.wildcard) { throw new Error(`can't define "${node.getSegments()}" after "${child.getSegments()}"`); } if (child.name !== node.name) { throw new Error(`invalid pattern name "${node.name}", as prev defined "${child.getSegments()}"`); } return child; } if (child.suffix !== node.suffix) { continue; } if (!node.wildcard && ((child.regex == null && node.regex == null) || (child.regex != null && node.regex != null && child.regex.toString() === node.regex.toString()))) { if (child.name !== node.name) { throw new Error(`invalid pattern name "${node.name}", as prev defined "${child.getSegments()}"`); } return child; } } parent.varyChildren.push(node); if (parent.varyChildren.length > 1) { parent.varyChildren.sort((a, b) => { if (a.suffix !== '' && b.suffix === '') { return 0; } if (a.suffix === '' && b.suffix !== '') { return 1; } if (a.regex == null && b.regex != null) { return 1; } return 0; }); } } else if (segment[0] === '*' || segment[0] === '(' || segment[0] === ')') { throw new Error(`Invalid pattern: "${node.getSegments()}"`); } else { parent.children[_segment] = node; } return node; } exports.default = Trie; //# sourceMappingURL=index.js.map