path-parser
Version:
A small utility to parse, match and generate paths
416 lines (338 loc) • 11.6 kB
JavaScript
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
;