UNPKG

9.87 kBJavaScriptView Raw
1/**
2 * TS adaption of https://github.com/pillarjs/path-to-regexp/blob/master/index.js
3 */
4/**
5 * Default configs.
6 */
7const DEFAULT_DELIMITER = '/';
8const DEFAULT_DELIMITERS = './';
9/**
10 * The main path matching regexp utility.
11 */
12const PATH_REGEXP = new RegExp([
13 // Match escaped characters that would otherwise appear in future matches.
14 // This allows the user to escape special characters that won't transform.
15 '(\\\\.)',
16 // Match Express-style parameters and un-named parameters with a prefix
17 // and optional suffixes. Matches appear as:
18 //
19 // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?"]
20 // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined]
21 '(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?'
22].join('|'), 'g');
23/**
24 * Parse a string for the raw tokens.
25 */
26export const parse = (str, options) => {
27 var tokens = [];
28 var key = 0;
29 var index = 0;
30 var path = '';
31 var defaultDelimiter = (options && options.delimiter) || DEFAULT_DELIMITER;
32 var delimiters = (options && options.delimiters) || DEFAULT_DELIMITERS;
33 var pathEscaped = false;
34 var res;
35 while ((res = PATH_REGEXP.exec(str)) !== null) {
36 var m = res[0];
37 var escaped = res[1];
38 var offset = res.index;
39 path += str.slice(index, offset);
40 index = offset + m.length;
41 // Ignore already escaped sequences.
42 if (escaped) {
43 path += escaped[1];
44 pathEscaped = true;
45 continue;
46 }
47 var prev = '';
48 var next = str[index];
49 var name = res[2];
50 var capture = res[3];
51 var group = res[4];
52 var modifier = res[5];
53 if (!pathEscaped && path.length) {
54 var k = path.length - 1;
55 if (delimiters.indexOf(path[k]) > -1) {
56 prev = path[k];
57 path = path.slice(0, k);
58 }
59 }
60 // Push the current path onto the tokens.
61 if (path) {
62 tokens.push(path);
63 path = '';
64 pathEscaped = false;
65 }
66 var partial = prev !== '' && next !== undefined && next !== prev;
67 var repeat = modifier === '+' || modifier === '*';
68 var optional = modifier === '?' || modifier === '*';
69 var delimiter = prev || defaultDelimiter;
70 var pattern = capture || group;
71 tokens.push({
72 name: name || key++,
73 prefix: prev,
74 delimiter: delimiter,
75 optional: optional,
76 repeat: repeat,
77 partial: partial,
78 pattern: pattern ? escapeGroup(pattern) : '[^' + escapeString(delimiter) + ']+?'
79 });
80 }
81 // Push any remaining characters.
82 if (path || index < str.length) {
83 tokens.push(path + str.substr(index));
84 }
85 return tokens;
86};
87/**
88 * Compile a string to a template function for the path.
89 */
90export const compile = (str, options) => {
91 return tokensToFunction(parse(str, options));
92};
93/**
94 * Expose a method for transforming tokens into the path function.
95 */
96export const tokensToFunction = (tokens) => {
97 // Compile all the tokens into regexps.
98 var matches = new Array(tokens.length);
99 // Compile all the patterns before compilation.
100 for (var i = 0; i < tokens.length; i++) {
101 var token = tokens[i];
102 if (typeof token === 'object') {
103 matches[i] = new RegExp('^(?:' + token.pattern + ')$');
104 }
105 }
106 return (data, options) => {
107 var path = '';
108 var encode = (options && options.encode) || encodeURIComponent;
109 for (var i = 0; i < tokens.length; i++) {
110 var token = tokens[i];
111 if (typeof token === 'string') {
112 path += token;
113 continue;
114 }
115 var value = data ? data[token.name] : undefined;
116 var segment;
117 if (Array.isArray(value)) {
118 if (!token.repeat) {
119 throw new TypeError('Expected "' + token.name + '" to not repeat, but got array');
120 }
121 if (value.length === 0) {
122 if (token.optional)
123 continue;
124 throw new TypeError('Expected "' + token.name + '" to not be empty');
125 }
126 for (var j = 0; j < value.length; j++) {
127 segment = encode(value[j]);
128 if (!matches[i].test(segment)) {
129 throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '"');
130 }
131 path += (j === 0 ? token.prefix : token.delimiter) + segment;
132 }
133 continue;
134 }
135 if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
136 segment = encode(String(value));
137 if (!matches[i].test(segment)) {
138 throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but got "' + segment + '"');
139 }
140 path += token.prefix + segment;
141 continue;
142 }
143 if (token.optional) {
144 // Prepend partial segment prefixes.
145 if (token.partial)
146 path += token.prefix;
147 continue;
148 }
149 throw new TypeError('Expected "' + token.name + '" to be ' + (token.repeat ? 'an array' : 'a string'));
150 }
151 return path;
152 };
153};
154/**
155 * Escape a regular expression string.
156 */
157const escapeString = (str) => {
158 return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1');
159};
160/**
161 * Escape the capturing group by escaping special characters and meaning.
162 */
163const escapeGroup = (group) => {
164 return group.replace(/([=!:$/()])/g, '\\$1');
165};
166/**
167 * Get the flags for a regexp from the options.
168 */
169const flags = (options) => {
170 return options && options.sensitive ? '' : 'i';
171};
172/**
173 * Pull out keys from a regexp.
174 */
175const regexpToRegexp = (path, keys) => {
176 if (!keys)
177 return path;
178 // Use a negative lookahead to match only capturing groups.
179 var groups = path.source.match(/\((?!\?)/g);
180 if (groups) {
181 for (var i = 0; i < groups.length; i++) {
182 keys.push({
183 name: i,
184 prefix: null,
185 delimiter: null,
186 optional: false,
187 repeat: false,
188 partial: false,
189 pattern: null
190 });
191 }
192 }
193 return path;
194};
195/**
196 * Transform an array into a regexp.
197 */
198const arrayToRegexp = (path, keys, options) => {
199 var parts = [];
200 for (var i = 0; i < path.length; i++) {
201 parts.push(pathToRegexp(path[i], keys, options).source);
202 }
203 return new RegExp('(?:' + parts.join('|') + ')', flags(options));
204};
205/**
206 * Create a path regexp from string input.
207 */
208const stringToRegexp = (path, keys, options) => {
209 return tokensToRegExp(parse(path, options), keys, options);
210};
211/**
212 * Expose a function for taking tokens and returning a RegExp.
213 */
214export const tokensToRegExp = (tokens, keys, options) => {
215 options = options || {};
216 var strict = options.strict;
217 var end = options.end !== false;
218 var delimiter = escapeString(options.delimiter || DEFAULT_DELIMITER);
219 var delimiters = options.delimiters || DEFAULT_DELIMITERS;
220 var endsWith = [].concat(options.endsWith || []).map(escapeString).concat('$').join('|');
221 var route = '';
222 var isEndDelimited = false;
223 // Iterate over the tokens and create our regexp string.
224 for (var i = 0; i < tokens.length; i++) {
225 var token = tokens[i];
226 if (typeof token === 'string') {
227 route += escapeString(token);
228 isEndDelimited = i === tokens.length - 1 && delimiters.indexOf(token[token.length - 1]) > -1;
229 }
230 else {
231 var prefix = escapeString(token.prefix || '');
232 var capture = token.repeat
233 ? '(?:' + token.pattern + ')(?:' + prefix + '(?:' + token.pattern + '))*'
234 : token.pattern;
235 if (keys)
236 keys.push(token);
237 if (token.optional) {
238 if (token.partial) {
239 route += prefix + '(' + capture + ')?';
240 }
241 else {
242 route += '(?:' + prefix + '(' + capture + '))?';
243 }
244 }
245 else {
246 route += prefix + '(' + capture + ')';
247 }
248 }
249 }
250 if (end) {
251 if (!strict)
252 route += '(?:' + delimiter + ')?';
253 route += endsWith === '$' ? '$' : '(?=' + endsWith + ')';
254 }
255 else {
256 if (!strict)
257 route += '(?:' + delimiter + '(?=' + endsWith + '))?';
258 if (!isEndDelimited)
259 route += '(?=' + delimiter + '|' + endsWith + ')';
260 }
261 return new RegExp('^' + route, flags(options));
262};
263/**
264 * Normalize the given path string, returning a regular expression.
265 *
266 * An empty array can be passed in for the keys, which will hold the
267 * placeholder key descriptions. For example, using `/user/:id`, `keys` will
268 * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
269 */
270export const pathToRegexp = (path, keys, options) => {
271 if (path instanceof RegExp) {
272 return regexpToRegexp(path, keys);
273 }
274 if (Array.isArray(path)) {
275 return arrayToRegexp(path, keys, options);
276 }
277 return stringToRegexp(path, keys, options);
278};