UNPKG

path-parser

Version:

A small utility to parse, match and generate paths

416 lines (338 loc) 11.6 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var tslib = require('tslib'); var searchParams = require('search-params'); /** * We encode using encodeURIComponent but we want to * preserver certain characters which are commonly used * (sub delimiters and ':') * * https://www.ietf.org/rfc/rfc3986.txt * * reserved = gen-delims / sub-delims * * gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" * * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" */ var excludeSubDelimiters = /[^!$'()*+,;|:]/g; var encodeURIComponentExcludingSubDelims = function encodeURIComponentExcludingSubDelims(segment) { return segment.replace(excludeSubDelimiters, function (match) { return encodeURIComponent(match); }); }; var encodingMethods = { "default": encodeURIComponentExcludingSubDelims, uri: encodeURI, uriComponent: encodeURIComponent, none: function none(val) { return val; }, legacy: encodeURI }; var decodingMethods = { "default": decodeURIComponent, uri: decodeURI, uriComponent: decodeURIComponent, none: function none(val) { return val; }, legacy: decodeURIComponent }; var encodeParam = function encodeParam(param, encoding, isSpatParam) { var encoder = encodingMethods[encoding] || encodeURIComponentExcludingSubDelims; if (isSpatParam) { return String(param).split('/').map(encoder).join('/'); } return encoder(String(param)); }; var decodeParam = function decodeParam(param, encoding) { return (decodingMethods[encoding] || decodeURIComponent)(param); }; var defaultOrConstrained = function defaultOrConstrained(match) { return '(' + (match ? match.replace(/(^<|>$)/g, '') : "[a-zA-Z0-9-_.~%':|=+\\*@$]+") + ')'; }; var rules = [{ name: 'url-parameter', pattern: /^:([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})(<(.+?)>)?/, regex: function regex(match) { return new RegExp(defaultOrConstrained(match[2])); } }, { name: 'url-parameter-splat', pattern: /^\*([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})/, regex: /([^?]*)/ }, { name: 'url-parameter-matrix', pattern: /^;([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})(<(.+?)>)?/, regex: function regex(match) { return new RegExp(';' + match[1] + '=' + defaultOrConstrained(match[2])); } }, { name: 'query-parameter', pattern: /^(?:\?|&)(?::)?([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})/ }, { name: 'delimiter', pattern: /^(\/|\?)/, regex: function regex(match) { return new RegExp('\\' + match[0]); } }, { name: 'sub-delimiter', pattern: /^(!|&|-|_|\.|;)/, regex: function regex(match) { return new RegExp(match[0]); } }, { name: 'fragment', pattern: /^([0-9a-zA-Z]+)/, regex: function regex(match) { return new RegExp(match[0]); } }]; var tokenise = function tokenise(str, tokens) { if (tokens === void 0) { tokens = []; } // Look for a matching rule var matched = rules.some(function (rule) { var match = str.match(rule.pattern); if (!match) { return false; } tokens.push({ type: rule.name, match: match[0], val: match.slice(1, 2), otherVal: match.slice(2), regex: rule.regex instanceof Function ? rule.regex(match) : rule.regex }); if (match[0].length < str.length) { tokens = tokenise(str.substr(match[0].length), tokens); } return true; }); // If no rules matched, throw an error (possible malformed path) if (!matched) { throw new Error("Could not parse path '" + str + "'"); } return tokens; }; var exists = function exists(val) { return val !== undefined && val !== null; }; var optTrailingSlash = function optTrailingSlash(source, strictTrailingSlash) { if (strictTrailingSlash) { return source; } if (source === '\\/') { return source; } return source.replace(/\\\/$/, '') + '(?:\\/)?'; }; var upToDelimiter = function upToDelimiter(source, delimiter) { if (!delimiter) { return source; } return /(\/)$/.test(source) ? source : source + '(\\/|\\?|\\.|;|$)'; }; var appendQueryParam = function appendQueryParam(params, param, val) { if (val === void 0) { val = ''; } var existingVal = params[param]; if (existingVal === undefined) { params[param] = val; } else { params[param] = Array.isArray(existingVal) ? existingVal.concat(val) : [existingVal, val]; } return params; }; var defaultOptions = { urlParamsEncoding: 'default' }; var Path = /*#__PURE__*/ /** @class */ function () { function Path(path, options) { if (!path) { throw new Error('Missing path in Path constructor'); } this.path = path; this.options = tslib.__assign(tslib.__assign({}, defaultOptions), options); this.tokens = tokenise(path); this.hasUrlParams = this.tokens.filter(function (t) { return /^url-parameter/.test(t.type); }).length > 0; this.hasSpatParam = this.tokens.filter(function (t) { return /splat$/.test(t.type); }).length > 0; this.hasMatrixParams = this.tokens.filter(function (t) { return /matrix$/.test(t.type); }).length > 0; this.hasQueryParams = this.tokens.filter(function (t) { return /^query-parameter/.test(t.type); }).length > 0; // Extract named parameters from tokens this.spatParams = this.getParams('url-parameter-splat'); this.urlParams = this.getParams(/^url-parameter/); // Query params this.queryParams = this.getParams('query-parameter'); // All params this.params = this.urlParams.concat(this.queryParams); // Check if hasQueryParams // Regular expressions for url part only (full and partial match) this.source = this.tokens.filter(function (t) { return t.regex !== undefined; }).map(function (t) { return t.regex.source; }).join(''); } Path.createPath = function (path, options) { return new Path(path, options); }; Path.prototype.isQueryParam = function (name) { return this.queryParams.indexOf(name) !== -1; }; Path.prototype.isSpatParam = function (name) { return this.spatParams.indexOf(name) !== -1; }; Path.prototype.test = function (path, opts) { var _this = this; var options = tslib.__assign(tslib.__assign({ caseSensitive: false, strictTrailingSlash: false }, this.options), opts); // trailingSlash: falsy => non optional, truthy => optional var source = optTrailingSlash(this.source, options.strictTrailingSlash); // Check if exact match var match = this.urlTest(path, source + (this.hasQueryParams ? '(\\?.*$|$)' : '$'), options.caseSensitive, options.urlParamsEncoding); // If no match, or no query params, no need to go further if (!match || !this.hasQueryParams) { return match; } // Extract query params var queryParams = searchParams.parse(path, options.queryParams); var unexpectedQueryParams = Object.keys(queryParams).filter(function (p) { return !_this.isQueryParam(p); }); if (unexpectedQueryParams.length === 0) { // Extend url match Object.keys(queryParams).forEach( // @ts-ignore function (p) { return match[p] = queryParams[p]; }); return match; } return null; }; Path.prototype.partialTest = function (path, opts) { var _this = this; var options = tslib.__assign(tslib.__assign({ caseSensitive: false, delimited: true }, this.options), opts); // Check if partial match (start of given path matches regex) // trailingSlash: falsy => non optional, truthy => optional var source = upToDelimiter(this.source, options.delimited); var match = this.urlTest(path, source, options.caseSensitive, options.urlParamsEncoding); if (!match) { return match; } if (!this.hasQueryParams) { return match; } var queryParams = searchParams.parse(path, options.queryParams); Object.keys(queryParams).filter(function (p) { return _this.isQueryParam(p); }).forEach(function (p) { return appendQueryParam(match, p, queryParams[p]); }); return match; }; Path.prototype.build = function (params, opts) { var _this = this; if (params === void 0) { params = {}; } var options = tslib.__assign(tslib.__assign({ ignoreConstraints: false, ignoreSearch: false, queryParams: {} }, this.options), opts); var encodedUrlParams = Object.keys(params).filter(function (p) { return !_this.isQueryParam(p); }).reduce(function (acc, key) { if (!exists(params[key])) { return acc; } var val = params[key]; var isSpatParam = _this.isSpatParam(key); if (typeof val === 'boolean') { acc[key] = val; } else if (Array.isArray(val)) { acc[key] = val.map(function (v) { return encodeParam(v, options.urlParamsEncoding, isSpatParam); }); } else { acc[key] = encodeParam(val, options.urlParamsEncoding, isSpatParam); } return acc; }, {}); // Check all params are provided (not search parameters which are optional) if (this.urlParams.some(function (p) { return !exists(params[p]); })) { var missingParameters = this.urlParams.filter(function (p) { return !exists(params[p]); }); throw new Error("Cannot build path: '" + this.path + "' requires missing parameters { " + missingParameters.join(', ') + ' }'); } // Check constraints if (!options.ignoreConstraints) { var constraintsPassed = this.tokens.filter(function (t) { return /^url-parameter/.test(t.type) && !/-splat$/.test(t.type); }).every(function (t) { return new RegExp('^' + defaultOrConstrained(t.otherVal[0]) + '$').test(encodedUrlParams[t.val]); }); if (!constraintsPassed) { throw new Error("Some parameters of '" + this.path + "' are of invalid format"); } } var base = this.tokens.filter(function (t) { return /^query-parameter/.test(t.type) === false; }).map(function (t) { if (t.type === 'url-parameter-matrix') { return ";" + t.val + "=" + encodedUrlParams[t.val[0]]; } return /^url-parameter/.test(t.type) ? encodedUrlParams[t.val[0]] : t.match; }).join(''); if (options.ignoreSearch) { return base; } var searchParams$1 = this.queryParams.filter(function (p) { return Object.keys(params).indexOf(p) !== -1; }).reduce(function (sparams, paramName) { sparams[paramName] = params[paramName]; return sparams; }, {}); var searchPart = searchParams.build(searchParams$1, options.queryParams); return searchPart ? base + '?' + searchPart : base; }; Path.prototype.getParams = function (type) { var predicate = type instanceof RegExp ? function (t) { return type.test(t.type); } : function (t) { return t.type === type; }; return this.tokens.filter(predicate).map(function (t) { return t.val[0]; }); }; Path.prototype.urlTest = function (path, source, caseSensitive, urlParamsEncoding) { var _this = this; var regex = new RegExp('^' + source, caseSensitive ? '' : 'i'); var match = path.match(regex); if (!match) { return null; } else if (!this.urlParams.length) { return {}; } // Reduce named params to key-value pairs return match.slice(1, this.urlParams.length + 1).reduce(function (params, m, i) { params[_this.urlParams[i]] = decodeParam(m, urlParamsEncoding); return params; }, {}); }; return Path; }(); exports.Path = Path; //# sourceMappingURL=path-parser.cjs.development.js.map