@peter.naydenov/url-pattern 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/url-pattern.cjs.js +52 -18
- package/dist/url-pattern.es.js +52 -18
- package/dist/url-pattern.umd.js +1 -1
- package/package.json +1 -1
- package/readme.md +21 -5
- package/src/main.js +51 -17
- package/types/main.d.ts +9 -4
package/dist/url-pattern.cjs.js
CHANGED
|
@@ -9,14 +9,15 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @typedef {Object} UrlPatternOptions
|
|
12
|
-
* @property {string} [escapeChar='\\'] - Character used for escaping
|
|
12
|
+
* @property {string} [escapeChar='\\'] - Character used for escaping. Only escapes regex metacharacters (`^$.*+?()[]{}|\`); for any other character the backslash is treated as a literal.
|
|
13
13
|
* @property {string} [segmentNameStartChar=':'] - Character that starts a named segment
|
|
14
14
|
* @property {string} [segmentNameEndChar] - Character that ends a named segment. When set, the segment name stops at the first occurrence of this character (instead of stopping at the first character outside `segmentNameCharset`).
|
|
15
15
|
* @property {string} [segmentNameCharset='a-zA-Z0-9_'] - Characters allowed in segment names
|
|
16
16
|
* @property {string} [segmentValueCharset='a-zA-Z0-9-_~ %'] - Characters allowed in segment values
|
|
17
17
|
* @property {string} [optionalSegmentStartChar='('] - Character that starts an optional segment
|
|
18
18
|
* @property {string} [optionalSegmentEndChar=')'] - Character that ends an optional segment
|
|
19
|
-
* @property {string} [wildcardChar='*'] - Character that denotes a wildcard
|
|
19
|
+
* @property {string} [wildcardChar='*'] - Character that denotes a wildcard in the pattern
|
|
20
|
+
* @property {string} [wildcardName='_'] - Key under which the wildcard value is stored in the match result
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -59,7 +60,8 @@ const DEFAULT_OPTIONS = {
|
|
|
59
60
|
segmentValueCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~ %',
|
|
60
61
|
optionalSegmentStartChar: '(',
|
|
61
62
|
optionalSegmentEndChar: ')',
|
|
62
|
-
wildcardChar: '*'
|
|
63
|
+
wildcardChar: '*',
|
|
64
|
+
wildcardName: '_'
|
|
63
65
|
};
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -153,10 +155,10 @@ const parsePattern = (pattern, options) => {
|
|
|
153
155
|
}
|
|
154
156
|
const nextChar = pattern[i + 1];
|
|
155
157
|
// Only treat \ as an escape when followed by a regex metacharacter.
|
|
156
|
-
// For
|
|
157
|
-
// is just a backslash followed by '
|
|
158
|
-
//
|
|
159
|
-
const regexMetachars = '
|
|
158
|
+
// For any other character the backslash is literal (e.g. '\:' in a URL
|
|
159
|
+
// is just a backslash followed by ':'). This means `:` cannot be escaped
|
|
160
|
+
// — it has no special meaning in regex, so the escape adds nothing.
|
|
161
|
+
const regexMetachars = '^$.*+?()[]{}|\\';
|
|
160
162
|
if (!regexMetachars.includes(nextChar)) {
|
|
161
163
|
// Not a regex metachar: treat the backslash as a literal character,
|
|
162
164
|
// advance past it so the next character is processed normally.
|
|
@@ -168,6 +170,7 @@ const parsePattern = (pattern, options) => {
|
|
|
168
170
|
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
169
171
|
});
|
|
170
172
|
i += 1;
|
|
173
|
+
continue;
|
|
171
174
|
} else {
|
|
172
175
|
segments.push({
|
|
173
176
|
type: 'literal',
|
|
@@ -202,7 +205,7 @@ const parsePattern = (pattern, options) => {
|
|
|
202
205
|
if (char === options.wildcardChar) {
|
|
203
206
|
segments.push({
|
|
204
207
|
type: 'wildcard',
|
|
205
|
-
name:
|
|
208
|
+
name: options.wildcardName,
|
|
206
209
|
regex: '.*',
|
|
207
210
|
optional: inOptional,
|
|
208
211
|
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
@@ -278,7 +281,6 @@ const parsePattern = (pattern, options) => {
|
|
|
278
281
|
return segments;
|
|
279
282
|
};
|
|
280
283
|
|
|
281
|
-
/**
|
|
282
284
|
/**
|
|
283
285
|
* Returns true when a value should be treated as "absent" for an optional segment.
|
|
284
286
|
* @param {*} val - Value to inspect
|
|
@@ -286,6 +288,9 @@ const parsePattern = (pattern, options) => {
|
|
|
286
288
|
*/
|
|
287
289
|
const isAbsentValue = (val) => {
|
|
288
290
|
if (val === undefined || val === null || val === '') return true;
|
|
291
|
+
// Number.isNaN catches numeric NaN values that would otherwise stringify to
|
|
292
|
+
// the literal string "NaN" in a generated URL.
|
|
293
|
+
if (typeof val === 'number' && Number.isNaN(val)) return true;
|
|
289
294
|
if (Array.isArray(val) && val.length === 0) return true;
|
|
290
295
|
return false;
|
|
291
296
|
};
|
|
@@ -296,7 +301,7 @@ const isAbsentValue = (val) => {
|
|
|
296
301
|
* @param {UrlPatternOptions} options - Options
|
|
297
302
|
* @returns {{regex: string, segmentNames: Array<SegmentName>}} Compiled regex and segment names
|
|
298
303
|
*/
|
|
299
|
-
const compileRegex = (segments,
|
|
304
|
+
const compileRegex = (segments, options) => {
|
|
300
305
|
let regex = '^';
|
|
301
306
|
let groupIndex = 0;
|
|
302
307
|
/** @type {Array<SegmentName>} */
|
|
@@ -316,7 +321,7 @@ const compileRegex = (segments, _options) => {
|
|
|
316
321
|
|
|
317
322
|
if (seg.type === 'wildcard') {
|
|
318
323
|
optionalPart += '(.*)';
|
|
319
|
-
segmentNames.push({ name:
|
|
324
|
+
segmentNames.push({ name: options.wildcardName, index: groupIndex, type: 'wildcard' });
|
|
320
325
|
groupIndex++;
|
|
321
326
|
} else if (seg.type === 'named') {
|
|
322
327
|
optionalPart += seg.regex;
|
|
@@ -336,7 +341,7 @@ const compileRegex = (segments, _options) => {
|
|
|
336
341
|
|
|
337
342
|
if (segment.type === 'wildcard') {
|
|
338
343
|
regex += '(.*)';
|
|
339
|
-
segmentNames.push({ name:
|
|
344
|
+
segmentNames.push({ name: options.wildcardName, index: groupIndex, type: 'wildcard' });
|
|
340
345
|
groupIndex++;
|
|
341
346
|
} else if (segment.type === 'named') {
|
|
342
347
|
regex += segment.regex;
|
|
@@ -363,7 +368,7 @@ const compileRegex = (segments, _options) => {
|
|
|
363
368
|
const makePattern = (pattern, options = {}) => {
|
|
364
369
|
const mergedOptions = mergeOptions(options);
|
|
365
370
|
const segments = parsePattern(pattern, mergedOptions);
|
|
366
|
-
const { regex, segmentNames } = compileRegex(segments);
|
|
371
|
+
const { regex, segmentNames } = compileRegex(segments, mergedOptions);
|
|
367
372
|
|
|
368
373
|
return {
|
|
369
374
|
regex,
|
|
@@ -383,9 +388,20 @@ const makePattern = (pattern, options = {}) => {
|
|
|
383
388
|
* @returns {CompiledPattern} Compiled pattern
|
|
384
389
|
*/
|
|
385
390
|
const makePatternFromRegex = (regex, keys = []) => {
|
|
391
|
+
// Strip the `g` and `y` flags. The `g` flag makes `RegExp.prototype.exec`
|
|
392
|
+
// advance `lastIndex` between calls, which would cause subsequent matches
|
|
393
|
+
// to start from the wrong position. The `y` (sticky) flag requires a match
|
|
394
|
+
// starting exactly at `lastIndex`, which is incompatible with our
|
|
395
|
+
// anchored, single-match contract. Other flags (`i`, `m`, `s`, `d`, `u`)
|
|
396
|
+
// are preserved.
|
|
397
|
+
const safeFlags = regex.flags.replace(/[gy]/g, '');
|
|
398
|
+
const regexObj = safeFlags === regex.flags
|
|
399
|
+
? regex
|
|
400
|
+
: new RegExp(regex.source, safeFlags);
|
|
401
|
+
|
|
386
402
|
return {
|
|
387
403
|
regex: regex.source,
|
|
388
|
-
regexObj
|
|
404
|
+
regexObj,
|
|
389
405
|
segments: [],
|
|
390
406
|
segmentNames: keys.map((name, index) => ({ name, index, type: 'named' })),
|
|
391
407
|
options: DEFAULT_OPTIONS,
|
|
@@ -401,6 +417,14 @@ const makePatternFromRegex = (regex, keys = []) => {
|
|
|
401
417
|
* @returns {Object|null} Extracted values or null if no match
|
|
402
418
|
*/
|
|
403
419
|
const match = (compiled, str) => {
|
|
420
|
+
// Guard against non-string input. `RegExp.prototype.exec` coerces with
|
|
421
|
+
// String(), so `match(123)` would silently try to match "123" — almost
|
|
422
|
+
// certainly not what the caller meant. Throw a TypeError so the misuse
|
|
423
|
+
// surfaces immediately.
|
|
424
|
+
if (typeof str !== 'string') {
|
|
425
|
+
throw new TypeError(`pattern.match() requires a string, got ${typeof str}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
404
428
|
const matchResult = compiled.regexObj.exec(str);
|
|
405
429
|
|
|
406
430
|
if (!matchResult) {
|
|
@@ -420,6 +444,10 @@ const match = (compiled, str) => {
|
|
|
420
444
|
});
|
|
421
445
|
return result;
|
|
422
446
|
}
|
|
447
|
+
// No keys, no groups → return the captured values as an array. If the
|
|
448
|
+
// regex had no capture groups, this is an empty array; we keep the
|
|
449
|
+
// existing behaviour (documented in the README's regex example) rather
|
|
450
|
+
// than coercing to `{}` and breaking callers that rely on the array.
|
|
423
451
|
return matchResult.slice(1);
|
|
424
452
|
}
|
|
425
453
|
|
|
@@ -509,9 +537,9 @@ const stringify = (compiled, values = {}) => {
|
|
|
509
537
|
}
|
|
510
538
|
optionalPart += Array.isArray(val) ? val.join('/') : val;
|
|
511
539
|
} else if (seg.type === 'wildcard') {
|
|
512
|
-
const val = values.
|
|
513
|
-
//
|
|
514
|
-
//
|
|
540
|
+
const val = values[compiled.options.wildcardName];
|
|
541
|
+
// Wildcards inside an optional group still consume values[wildcardName]
|
|
542
|
+
// when present; if absent, the whole group is wiped.
|
|
515
543
|
if (isAbsentValue(val)) {
|
|
516
544
|
optionalPart = '';
|
|
517
545
|
break;
|
|
@@ -543,7 +571,7 @@ const stringify = (compiled, values = {}) => {
|
|
|
543
571
|
}
|
|
544
572
|
result += Array.isArray(value) ? value.join('/') : value;
|
|
545
573
|
} else if (segment.type === 'wildcard') {
|
|
546
|
-
const value = values.
|
|
574
|
+
const value = values[compiled.options.wildcardName];
|
|
547
575
|
// Empty string is a valid wildcard match result (e.g. '/files/*' on '/files/')
|
|
548
576
|
// and should be allowed without throwing.
|
|
549
577
|
if (value === undefined || value === null) {
|
|
@@ -575,6 +603,12 @@ class UrlPattern {
|
|
|
575
603
|
/** @type {CompiledPattern} */
|
|
576
604
|
this.compiled = makePattern(pattern, /** @type {UrlPatternOptions} */ (options));
|
|
577
605
|
}
|
|
606
|
+
// The compiled state is exposed as a read-only introspection field; freeze
|
|
607
|
+
// it (and the nested `options` object) so accidental mutation fails loudly
|
|
608
|
+
// instead of silently desynchronising the cached regex. `Object.freeze`
|
|
609
|
+
// is shallow, so we explicitly freeze `options` too.
|
|
610
|
+
Object.freeze(this.compiled.options);
|
|
611
|
+
Object.freeze(this.compiled);
|
|
578
612
|
}
|
|
579
613
|
|
|
580
614
|
/**
|
package/dist/url-pattern.es.js
CHANGED
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {Object} UrlPatternOptions
|
|
8
|
-
* @property {string} [escapeChar='\\'] - Character used for escaping
|
|
8
|
+
* @property {string} [escapeChar='\\'] - Character used for escaping. Only escapes regex metacharacters (`^$.*+?()[]{}|\`); for any other character the backslash is treated as a literal.
|
|
9
9
|
* @property {string} [segmentNameStartChar=':'] - Character that starts a named segment
|
|
10
10
|
* @property {string} [segmentNameEndChar] - Character that ends a named segment. When set, the segment name stops at the first occurrence of this character (instead of stopping at the first character outside `segmentNameCharset`).
|
|
11
11
|
* @property {string} [segmentNameCharset='a-zA-Z0-9_'] - Characters allowed in segment names
|
|
12
12
|
* @property {string} [segmentValueCharset='a-zA-Z0-9-_~ %'] - Characters allowed in segment values
|
|
13
13
|
* @property {string} [optionalSegmentStartChar='('] - Character that starts an optional segment
|
|
14
14
|
* @property {string} [optionalSegmentEndChar=')'] - Character that ends an optional segment
|
|
15
|
-
* @property {string} [wildcardChar='*'] - Character that denotes a wildcard
|
|
15
|
+
* @property {string} [wildcardChar='*'] - Character that denotes a wildcard in the pattern
|
|
16
|
+
* @property {string} [wildcardName='_'] - Key under which the wildcard value is stored in the match result
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -55,7 +56,8 @@ const DEFAULT_OPTIONS = {
|
|
|
55
56
|
segmentValueCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~ %',
|
|
56
57
|
optionalSegmentStartChar: '(',
|
|
57
58
|
optionalSegmentEndChar: ')',
|
|
58
|
-
wildcardChar: '*'
|
|
59
|
+
wildcardChar: '*',
|
|
60
|
+
wildcardName: '_'
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
/**
|
|
@@ -149,10 +151,10 @@ const parsePattern = (pattern, options) => {
|
|
|
149
151
|
}
|
|
150
152
|
const nextChar = pattern[i + 1];
|
|
151
153
|
// Only treat \ as an escape when followed by a regex metacharacter.
|
|
152
|
-
// For
|
|
153
|
-
// is just a backslash followed by '
|
|
154
|
-
//
|
|
155
|
-
const regexMetachars = '
|
|
154
|
+
// For any other character the backslash is literal (e.g. '\:' in a URL
|
|
155
|
+
// is just a backslash followed by ':'). This means `:` cannot be escaped
|
|
156
|
+
// — it has no special meaning in regex, so the escape adds nothing.
|
|
157
|
+
const regexMetachars = '^$.*+?()[]{}|\\';
|
|
156
158
|
if (!regexMetachars.includes(nextChar)) {
|
|
157
159
|
// Not a regex metachar: treat the backslash as a literal character,
|
|
158
160
|
// advance past it so the next character is processed normally.
|
|
@@ -164,6 +166,7 @@ const parsePattern = (pattern, options) => {
|
|
|
164
166
|
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
165
167
|
});
|
|
166
168
|
i += 1;
|
|
169
|
+
continue;
|
|
167
170
|
} else {
|
|
168
171
|
segments.push({
|
|
169
172
|
type: 'literal',
|
|
@@ -198,7 +201,7 @@ const parsePattern = (pattern, options) => {
|
|
|
198
201
|
if (char === options.wildcardChar) {
|
|
199
202
|
segments.push({
|
|
200
203
|
type: 'wildcard',
|
|
201
|
-
name:
|
|
204
|
+
name: options.wildcardName,
|
|
202
205
|
regex: '.*',
|
|
203
206
|
optional: inOptional,
|
|
204
207
|
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
@@ -274,7 +277,6 @@ const parsePattern = (pattern, options) => {
|
|
|
274
277
|
return segments;
|
|
275
278
|
};
|
|
276
279
|
|
|
277
|
-
/**
|
|
278
280
|
/**
|
|
279
281
|
* Returns true when a value should be treated as "absent" for an optional segment.
|
|
280
282
|
* @param {*} val - Value to inspect
|
|
@@ -282,6 +284,9 @@ const parsePattern = (pattern, options) => {
|
|
|
282
284
|
*/
|
|
283
285
|
const isAbsentValue = (val) => {
|
|
284
286
|
if (val === undefined || val === null || val === '') return true;
|
|
287
|
+
// Number.isNaN catches numeric NaN values that would otherwise stringify to
|
|
288
|
+
// the literal string "NaN" in a generated URL.
|
|
289
|
+
if (typeof val === 'number' && Number.isNaN(val)) return true;
|
|
285
290
|
if (Array.isArray(val) && val.length === 0) return true;
|
|
286
291
|
return false;
|
|
287
292
|
};
|
|
@@ -292,7 +297,7 @@ const isAbsentValue = (val) => {
|
|
|
292
297
|
* @param {UrlPatternOptions} options - Options
|
|
293
298
|
* @returns {{regex: string, segmentNames: Array<SegmentName>}} Compiled regex and segment names
|
|
294
299
|
*/
|
|
295
|
-
const compileRegex = (segments,
|
|
300
|
+
const compileRegex = (segments, options) => {
|
|
296
301
|
let regex = '^';
|
|
297
302
|
let groupIndex = 0;
|
|
298
303
|
/** @type {Array<SegmentName>} */
|
|
@@ -312,7 +317,7 @@ const compileRegex = (segments, _options) => {
|
|
|
312
317
|
|
|
313
318
|
if (seg.type === 'wildcard') {
|
|
314
319
|
optionalPart += '(.*)';
|
|
315
|
-
segmentNames.push({ name:
|
|
320
|
+
segmentNames.push({ name: options.wildcardName, index: groupIndex, type: 'wildcard' });
|
|
316
321
|
groupIndex++;
|
|
317
322
|
} else if (seg.type === 'named') {
|
|
318
323
|
optionalPart += seg.regex;
|
|
@@ -332,7 +337,7 @@ const compileRegex = (segments, _options) => {
|
|
|
332
337
|
|
|
333
338
|
if (segment.type === 'wildcard') {
|
|
334
339
|
regex += '(.*)';
|
|
335
|
-
segmentNames.push({ name:
|
|
340
|
+
segmentNames.push({ name: options.wildcardName, index: groupIndex, type: 'wildcard' });
|
|
336
341
|
groupIndex++;
|
|
337
342
|
} else if (segment.type === 'named') {
|
|
338
343
|
regex += segment.regex;
|
|
@@ -359,7 +364,7 @@ const compileRegex = (segments, _options) => {
|
|
|
359
364
|
const makePattern = (pattern, options = {}) => {
|
|
360
365
|
const mergedOptions = mergeOptions(options);
|
|
361
366
|
const segments = parsePattern(pattern, mergedOptions);
|
|
362
|
-
const { regex, segmentNames } = compileRegex(segments);
|
|
367
|
+
const { regex, segmentNames } = compileRegex(segments, mergedOptions);
|
|
363
368
|
|
|
364
369
|
return {
|
|
365
370
|
regex,
|
|
@@ -379,9 +384,20 @@ const makePattern = (pattern, options = {}) => {
|
|
|
379
384
|
* @returns {CompiledPattern} Compiled pattern
|
|
380
385
|
*/
|
|
381
386
|
const makePatternFromRegex = (regex, keys = []) => {
|
|
387
|
+
// Strip the `g` and `y` flags. The `g` flag makes `RegExp.prototype.exec`
|
|
388
|
+
// advance `lastIndex` between calls, which would cause subsequent matches
|
|
389
|
+
// to start from the wrong position. The `y` (sticky) flag requires a match
|
|
390
|
+
// starting exactly at `lastIndex`, which is incompatible with our
|
|
391
|
+
// anchored, single-match contract. Other flags (`i`, `m`, `s`, `d`, `u`)
|
|
392
|
+
// are preserved.
|
|
393
|
+
const safeFlags = regex.flags.replace(/[gy]/g, '');
|
|
394
|
+
const regexObj = safeFlags === regex.flags
|
|
395
|
+
? regex
|
|
396
|
+
: new RegExp(regex.source, safeFlags);
|
|
397
|
+
|
|
382
398
|
return {
|
|
383
399
|
regex: regex.source,
|
|
384
|
-
regexObj
|
|
400
|
+
regexObj,
|
|
385
401
|
segments: [],
|
|
386
402
|
segmentNames: keys.map((name, index) => ({ name, index, type: 'named' })),
|
|
387
403
|
options: DEFAULT_OPTIONS,
|
|
@@ -397,6 +413,14 @@ const makePatternFromRegex = (regex, keys = []) => {
|
|
|
397
413
|
* @returns {Object|null} Extracted values or null if no match
|
|
398
414
|
*/
|
|
399
415
|
const match = (compiled, str) => {
|
|
416
|
+
// Guard against non-string input. `RegExp.prototype.exec` coerces with
|
|
417
|
+
// String(), so `match(123)` would silently try to match "123" — almost
|
|
418
|
+
// certainly not what the caller meant. Throw a TypeError so the misuse
|
|
419
|
+
// surfaces immediately.
|
|
420
|
+
if (typeof str !== 'string') {
|
|
421
|
+
throw new TypeError(`pattern.match() requires a string, got ${typeof str}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
400
424
|
const matchResult = compiled.regexObj.exec(str);
|
|
401
425
|
|
|
402
426
|
if (!matchResult) {
|
|
@@ -416,6 +440,10 @@ const match = (compiled, str) => {
|
|
|
416
440
|
});
|
|
417
441
|
return result;
|
|
418
442
|
}
|
|
443
|
+
// No keys, no groups → return the captured values as an array. If the
|
|
444
|
+
// regex had no capture groups, this is an empty array; we keep the
|
|
445
|
+
// existing behaviour (documented in the README's regex example) rather
|
|
446
|
+
// than coercing to `{}` and breaking callers that rely on the array.
|
|
419
447
|
return matchResult.slice(1);
|
|
420
448
|
}
|
|
421
449
|
|
|
@@ -505,9 +533,9 @@ const stringify = (compiled, values = {}) => {
|
|
|
505
533
|
}
|
|
506
534
|
optionalPart += Array.isArray(val) ? val.join('/') : val;
|
|
507
535
|
} else if (seg.type === 'wildcard') {
|
|
508
|
-
const val = values.
|
|
509
|
-
//
|
|
510
|
-
//
|
|
536
|
+
const val = values[compiled.options.wildcardName];
|
|
537
|
+
// Wildcards inside an optional group still consume values[wildcardName]
|
|
538
|
+
// when present; if absent, the whole group is wiped.
|
|
511
539
|
if (isAbsentValue(val)) {
|
|
512
540
|
optionalPart = '';
|
|
513
541
|
break;
|
|
@@ -539,7 +567,7 @@ const stringify = (compiled, values = {}) => {
|
|
|
539
567
|
}
|
|
540
568
|
result += Array.isArray(value) ? value.join('/') : value;
|
|
541
569
|
} else if (segment.type === 'wildcard') {
|
|
542
|
-
const value = values.
|
|
570
|
+
const value = values[compiled.options.wildcardName];
|
|
543
571
|
// Empty string is a valid wildcard match result (e.g. '/files/*' on '/files/')
|
|
544
572
|
// and should be allowed without throwing.
|
|
545
573
|
if (value === undefined || value === null) {
|
|
@@ -571,6 +599,12 @@ class UrlPattern {
|
|
|
571
599
|
/** @type {CompiledPattern} */
|
|
572
600
|
this.compiled = makePattern(pattern, /** @type {UrlPatternOptions} */ (options));
|
|
573
601
|
}
|
|
602
|
+
// The compiled state is exposed as a read-only introspection field; freeze
|
|
603
|
+
// it (and the nested `options` object) so accidental mutation fails loudly
|
|
604
|
+
// instead of silently desynchronising the cached regex. `Object.freeze`
|
|
605
|
+
// is shallow, so we explicitly freeze `options` too.
|
|
606
|
+
Object.freeze(this.compiled.options);
|
|
607
|
+
Object.freeze(this.compiled);
|
|
574
608
|
}
|
|
575
609
|
|
|
576
610
|
/**
|
package/dist/url-pattern.umd.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).urlPattern={})}(this,function(e){"use strict";const t={escapeChar:"\\",segmentNameStartChar:":",segmentNameEndChar:void 0,segmentNameCharset:"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_",segmentValueCharset:"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~ %",optionalSegmentStartChar:"(",optionalSegmentEndChar:")",wildcardChar:"*"},n=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),r=e=>{let t=e.replace(/\\/g,"\\\\");return t=t.replace(/\]/g,"\\]"),t=t.replace(/\^/g,"\\^"),t=t.replace(/-/g,"\\-"),t},a=(e,t,n)=>{const r=[n.escapeChar,n.optionalSegmentStartChar,n.optionalSegmentEndChar,n.wildcardChar,n.segmentNameStartChar];let a=e.length;for(let n=0;n<r.length;n++){const o=r[n];if(!o)continue;const i=e.indexOf(o,t);-1!==i&&i<a&&(a=i)}return a},o=e=>null==e||""===e||!(!Array.isArray(e)||0!==e.length),i=(e,o={})=>{const i=((e={})=>({...t,...e}))(o),s=((e,t)=>{const o=[];let i=0,s=!1,l=0,p=0;for(;i<e.length;){const
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).urlPattern={})}(this,function(e){"use strict";const t={escapeChar:"\\",segmentNameStartChar:":",segmentNameEndChar:void 0,segmentNameCharset:"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_",segmentValueCharset:"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~ %",optionalSegmentStartChar:"(",optionalSegmentEndChar:")",wildcardChar:"*",wildcardName:"_"},n=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),r=e=>{let t=e.replace(/\\/g,"\\\\");return t=t.replace(/\]/g,"\\]"),t=t.replace(/\^/g,"\\^"),t=t.replace(/-/g,"\\-"),t},a=(e,t,n)=>{const r=[n.escapeChar,n.optionalSegmentStartChar,n.optionalSegmentEndChar,n.wildcardChar,n.segmentNameStartChar];let a=e.length;for(let n=0;n<r.length;n++){const o=r[n];if(!o)continue;const i=e.indexOf(o,t);-1!==i&&i<a&&(a=i)}return a},o=e=>null==e||""===e||(!("number"!=typeof e||!Number.isNaN(e))||!(!Array.isArray(e)||0!==e.length)),i=(e,o={})=>{const i=((e={})=>({...t,...e}))(o),s=((e,t)=>{const o=[];let i=0,s=!1,l=0,p=0;for(;i<e.length;){const d=e[i];if(d===t.escapeChar){if(i+1>=e.length)throw new Error(`Invalid pattern: '\\' at position ${i} has nothing to escape`);const t=e[i+1];if("^$.*+?()[]{}|\\".includes(t)){o.push({type:"literal",name:t,regex:n(t),optional:s,optionalGroupId:s?l:void 0}),i+=2;continue}o.push({type:"literal",name:"\\",regex:"\\\\",optional:s,optionalGroupId:s?l:void 0}),i+=1;continue}if(d===t.optionalSegmentStartChar){s=!0,l++,p++,i++;continue}if(d===t.optionalSegmentEndChar){if(0===p)throw new Error(`Invalid pattern: unmatched '${d}' at position ${i}`);s=!1,p--,i++;continue}if(d===t.wildcardChar){o.push({type:"wildcard",name:t.wildcardName,regex:".*",optional:s,optionalGroupId:s?l:void 0}),i++;continue}if(d===t.segmentNameStartChar&&i+1<e.length){const n=e.slice(i+1);let a=0;const p=t.segmentNameCharset||"",c=t.segmentNameEndChar;for(let e=0;e<n.length&&(!c||n[e]!==c)&&p.includes(n[e]);e++)a=e+1;const m=n.slice(0,a),g=!(!c||n[a]!==c);if(m.length>0){const e=t.segmentValueCharset||"",n=`([${r(e)}]+)`;o.push({type:"named",name:m,regex:n,optional:s,optionalGroupId:s?l:void 0}),i+=1+a+(g?1:0);continue}throw new Error(`Invalid pattern: '${d}' at position ${i} has no segment name`)}const c=a(e,i,t);if(c>i){const t=e.slice(i,c);o.push({type:"literal",name:t,regex:n(t),optional:s,optionalGroupId:s?l:void 0}),i=c;continue}throw new Error(`Invalid pattern: '${d}' at position ${i} has no segment name`)}if(0!==p)throw new Error(`Invalid pattern: unclosed '${t.optionalSegmentStartChar}'`);return o})(e,i),{regex:l,segmentNames:p}=((e,t)=>{let n="^",r=0;const a=[];let o=0;for(;o<e.length;){const i=e[o];if(i.optional){let s="",l=o;const p=i.optionalGroupId;for(;l<e.length&&e[l].optional&&e[l].optionalGroupId===p;){const n=e[l];"wildcard"===n.type?(s+="(.*)",a.push({name:t.wildcardName,index:r,type:"wildcard"}),r++):"named"===n.type?(s+=n.regex,a.push({name:n.name,index:r,type:"named"}),r++):s+=n.regex,l++}n+=`(?:${s})?`,o=l;continue}"wildcard"===i.type?(n+="(.*)",a.push({name:t.wildcardName,index:r,type:"wildcard"}),r++):"named"===i.type?(n+=i.regex,a.push({name:i.name,index:r,type:"named"}),r++):n+=i.regex,o++}return n+="$",{regex:n,segmentNames:a}})(s,i);return{regex:l,regexObj:new RegExp(l),segments:s,segmentNames:p,options:i,isRegex:!1,pattern:e}},s=(e,n=[])=>{const r=e.flags.replace(/[gy]/g,""),a=r===e.flags?e:new RegExp(e.source,r);return{regex:e.source,regexObj:a,segments:[],segmentNames:n.map((e,t)=>({name:e,index:t,type:"named"})),options:t,isRegex:!0,keys:n}},l=(e,t)=>{if("string"!=typeof t)throw new TypeError("pattern.match() requires a string, got "+typeof t);const n=e.regexObj.exec(t);if(!n)return null;if(e.isRegex){if(e.keys&&e.keys.length>0){const t={};return e.keys.forEach((e,r)=>{const a=n[r+1];void 0!==a&&(t[e]=a)}),t}return n.slice(1)}const r={},a=new Set,o=new Set;for(const t of e.segmentNames)"wildcard"===t.type&&o.add(t.name);for(let t=0;t<e.segmentNames.length;t++){const o=e.segmentNames[t],i=o.index+1 in n?n[o.index+1]:"",s=void 0===i?"":i;a.has(o.name)?(Array.isArray(r[o.name])||(r[o.name]=[r[o.name]]),r[o.name].push(s)):(a.add(o.name),r[o.name]=s)}for(const e in r){const t=r[e];if(Array.isArray(t)){const n=t.filter(t=>""!==t||o.has(e));0===n.length?delete r[e]:r[e]=n}else""!==t||o.has(e)||delete r[e]}return r},p=(e,t={})=>{if(e.isRegex)throw new Error("Cannot stringify a pattern created from regex");let n="",r=0;for(;r<e.segments.length;){const a=e.segments[r];if(a.optional){let i="",s=r;const l=a.optionalGroupId;for(;s<e.segments.length&&e.segments[s].optional&&e.segments[s].optionalGroupId===l;){const n=e.segments[s];if("literal"===n.type)i+=n.name;else if("named"===n.type){const e=t[n.name];if(o(e)){i="";break}i+=Array.isArray(e)?e.join("/"):e}else if("wildcard"===n.type){const n=t[e.options.wildcardName];if(o(n)){i="";break}i+=Array.isArray(n)?n.join("/"):n}s++}""!==i&&(n+=i),r===s?r++:r=s;continue}if("literal"===a.type)n+=a.name;else if("named"===a.type){const e=t[a.name];if(o(e))throw new Error(`Missing required value for segment: ${a.name}`);n+=Array.isArray(e)?e.join("/"):e}else if("wildcard"===a.type){const r=t[e.options.wildcardName];if(null==r)throw new Error("Missing required wildcard value");n+=Array.isArray(r)?r.join("/"):r}r++}return n};class d{constructor(e,t={}){if(e instanceof RegExp){const n=Array.isArray(t)?t:[];this.compiled=s(e,n)}else this.compiled=i(e,t);Object.freeze(this.compiled.options),Object.freeze(this.compiled)}match(e){return l(this.compiled,e)}stringify(e){return p(this.compiled,e)}}const c=(e,t={})=>new d(e,t);e.DEFAULT_OPTIONS=t,e.UrlPattern=d,e.default=c,e.makePattern=i,e.makePatternFromRegex=s,e.match=l,e.stringify=p,e.urlPattern=c,Object.defineProperty(e,"__esModule",{value:!0})});
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -106,20 +106,21 @@ Creates a new pattern instance.
|
|
|
106
106
|
|
|
107
107
|
#### Options
|
|
108
108
|
|
|
109
|
-
- **escapeChar** `{string}` - Character used for escaping
|
|
109
|
+
- **escapeChar** `{string}` - Character used for escaping (default: `'\\'`). Only escapes regex metacharacters (`^$.*+?()[]{}|\`); the backslash is kept literal for any other following character.
|
|
110
110
|
- **segmentNameStartChar** `{string}` - Character that starts a named segment (default: `':'`)
|
|
111
111
|
- **segmentNameEndChar** `{string}` - Character that ends a named segment (default: `undefined`). When set, the segment name stops at the first occurrence of this character instead of at the first character outside `segmentNameCharset`.
|
|
112
112
|
- **segmentNameCharset** `{string}` - Characters allowed in segment names (default: `'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'`). Treated as a list of explicit characters; range notation is not interpreted.
|
|
113
113
|
- **segmentValueCharset** `{string}` - Characters allowed in segment values (default: `'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~ %'`). Treated as a list of explicit characters; range notation is not interpreted.
|
|
114
114
|
- **optionalSegmentStartChar** `{string}` - Character that starts an optional segment (default: `'('`)
|
|
115
115
|
- **optionalSegmentEndChar** `{string}` - Character that ends an optional segment (default: `')'`)
|
|
116
|
-
- **wildcardChar** `{string}` - Character that denotes a wildcard (default: `'*'`)
|
|
116
|
+
- **wildcardChar** `{string}` - Character that denotes a wildcard in the pattern (default: `'*'`)
|
|
117
|
+
- **wildcardName** `{string}` - Key under which the wildcard value is stored in the match result (default: `'_'`). Change this to avoid colliding with a named segment that also uses `_`, or to give wildcards a more descriptive key.
|
|
117
118
|
|
|
118
119
|
### Pattern Methods
|
|
119
120
|
|
|
120
121
|
#### `pattern.match(string)`
|
|
121
122
|
|
|
122
|
-
Matches a string against the pattern and returns an object with captured values, or `null` if no match.
|
|
123
|
+
Matches a string against the pattern and returns an object with captured values, or `null` if no match. Throws a `TypeError` if the argument is not a string.
|
|
123
124
|
|
|
124
125
|
#### `pattern.stringify(data)`
|
|
125
126
|
|
|
@@ -127,14 +128,29 @@ Generates a URL string from provided data object.
|
|
|
127
128
|
|
|
128
129
|
#### `pattern.compiled`
|
|
129
130
|
|
|
130
|
-
Read-only object exposing the internal compiled state: `regex` (regex source), `regexObj` (compiled `RegExp`), `segments` (parsed segments array), `segmentNames` (segment name → capture-group mappings), `options` (merged options), `isRegex` (whether the pattern was created from a regex), and `pattern` (the original pattern string). Useful for introspection; do not mutate.
|
|
131
|
+
Read-only object exposing the internal compiled state: `regex` (regex source), `regexObj` (compiled `RegExp`), `segments` (parsed segments array), `segmentNames` (segment name → capture-group mappings), `options` (merged options), `isRegex` (whether the pattern was created from a regex), and `pattern` (the original pattern string). The object is frozen — any attempt to mutate it will throw in strict mode (or fail silently elsewhere). Useful for introspection; do not mutate.
|
|
132
|
+
|
|
133
|
+
## Notes
|
|
134
|
+
|
|
135
|
+
### Wildcard result key
|
|
136
|
+
|
|
137
|
+
Wildcards are stored under the key `_` by default. If a pattern has both a wildcard and a named segment that resolves to `_` (e.g. `/api/:_/files/*`), both values end up under the same key — the second one is appended, producing an array. To avoid this, configure the wildcard key with the `wildcardName` option:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
const pattern = urlPattern('/api/:id/files/*', { wildcardName: 'rest' })
|
|
141
|
+
pattern.match('/api/42/files/a/b') // → { id: '42', rest: 'a/b' }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Regex patterns
|
|
145
|
+
|
|
146
|
+
When you pass a `RegExp` to `urlPattern`, the `g` and `y` flags are stripped. They are incompatible with this library's anchored, single-match contract: `g` would make `exec` advance `lastIndex` between calls (so the second `match()` would return `null`); `y` (sticky) requires a match starting exactly at `lastIndex`. Other flags (`i`, `m`, `s`, `d`, `u`) are preserved. The original regex object you passed in is not mutated — the library creates a fresh `RegExp` internally.
|
|
131
147
|
|
|
132
148
|
|
|
133
149
|
|
|
134
150
|
## Links
|
|
135
151
|
- [Supported URL patterns with examples](./PATTERNS.md).
|
|
136
152
|
- [History of changes](CHANGELOG.md)
|
|
137
|
-
- [TypeScript definitions](types/
|
|
153
|
+
- [TypeScript definitions](types/main.d.ts)
|
|
138
154
|
|
|
139
155
|
|
|
140
156
|
|
package/src/main.js
CHANGED
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {Object} UrlPatternOptions
|
|
8
|
-
* @property {string} [escapeChar='\\'] - Character used for escaping
|
|
8
|
+
* @property {string} [escapeChar='\\'] - Character used for escaping. Only escapes regex metacharacters (`^$.*+?()[]{}|\`); for any other character the backslash is treated as a literal.
|
|
9
9
|
* @property {string} [segmentNameStartChar=':'] - Character that starts a named segment
|
|
10
10
|
* @property {string} [segmentNameEndChar] - Character that ends a named segment. When set, the segment name stops at the first occurrence of this character (instead of stopping at the first character outside `segmentNameCharset`).
|
|
11
11
|
* @property {string} [segmentNameCharset='a-zA-Z0-9_'] - Characters allowed in segment names
|
|
12
12
|
* @property {string} [segmentValueCharset='a-zA-Z0-9-_~ %'] - Characters allowed in segment values
|
|
13
13
|
* @property {string} [optionalSegmentStartChar='('] - Character that starts an optional segment
|
|
14
14
|
* @property {string} [optionalSegmentEndChar=')'] - Character that ends an optional segment
|
|
15
|
-
* @property {string} [wildcardChar='*'] - Character that denotes a wildcard
|
|
15
|
+
* @property {string} [wildcardChar='*'] - Character that denotes a wildcard in the pattern
|
|
16
|
+
* @property {string} [wildcardName='_'] - Key under which the wildcard value is stored in the match result
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -55,7 +56,8 @@ const DEFAULT_OPTIONS = {
|
|
|
55
56
|
segmentValueCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~ %',
|
|
56
57
|
optionalSegmentStartChar: '(',
|
|
57
58
|
optionalSegmentEndChar: ')',
|
|
58
|
-
wildcardChar: '*'
|
|
59
|
+
wildcardChar: '*',
|
|
60
|
+
wildcardName: '_'
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
/**
|
|
@@ -149,10 +151,10 @@ const parsePattern = (pattern, options) => {
|
|
|
149
151
|
}
|
|
150
152
|
const nextChar = pattern[i + 1];
|
|
151
153
|
// Only treat \ as an escape when followed by a regex metacharacter.
|
|
152
|
-
// For
|
|
153
|
-
// is just a backslash followed by '
|
|
154
|
-
//
|
|
155
|
-
const regexMetachars = '
|
|
154
|
+
// For any other character the backslash is literal (e.g. '\:' in a URL
|
|
155
|
+
// is just a backslash followed by ':'). This means `:` cannot be escaped
|
|
156
|
+
// — it has no special meaning in regex, so the escape adds nothing.
|
|
157
|
+
const regexMetachars = '^$.*+?()[]{}|\\';
|
|
156
158
|
if (!regexMetachars.includes(nextChar)) {
|
|
157
159
|
// Not a regex metachar: treat the backslash as a literal character,
|
|
158
160
|
// advance past it so the next character is processed normally.
|
|
@@ -164,6 +166,7 @@ const parsePattern = (pattern, options) => {
|
|
|
164
166
|
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
165
167
|
});
|
|
166
168
|
i += 1;
|
|
169
|
+
continue;
|
|
167
170
|
} else {
|
|
168
171
|
segments.push({
|
|
169
172
|
type: 'literal',
|
|
@@ -198,7 +201,7 @@ const parsePattern = (pattern, options) => {
|
|
|
198
201
|
if (char === options.wildcardChar) {
|
|
199
202
|
segments.push({
|
|
200
203
|
type: 'wildcard',
|
|
201
|
-
name:
|
|
204
|
+
name: options.wildcardName,
|
|
202
205
|
regex: '.*',
|
|
203
206
|
optional: inOptional,
|
|
204
207
|
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
@@ -274,7 +277,6 @@ const parsePattern = (pattern, options) => {
|
|
|
274
277
|
return segments;
|
|
275
278
|
};
|
|
276
279
|
|
|
277
|
-
/**
|
|
278
280
|
/**
|
|
279
281
|
* Returns true when a value should be treated as "absent" for an optional segment.
|
|
280
282
|
* @param {*} val - Value to inspect
|
|
@@ -282,6 +284,9 @@ const parsePattern = (pattern, options) => {
|
|
|
282
284
|
*/
|
|
283
285
|
const isAbsentValue = (val) => {
|
|
284
286
|
if (val === undefined || val === null || val === '') return true;
|
|
287
|
+
// Number.isNaN catches numeric NaN values that would otherwise stringify to
|
|
288
|
+
// the literal string "NaN" in a generated URL.
|
|
289
|
+
if (typeof val === 'number' && Number.isNaN(val)) return true;
|
|
285
290
|
if (Array.isArray(val) && val.length === 0) return true;
|
|
286
291
|
return false;
|
|
287
292
|
};
|
|
@@ -292,7 +297,7 @@ const isAbsentValue = (val) => {
|
|
|
292
297
|
* @param {UrlPatternOptions} options - Options
|
|
293
298
|
* @returns {{regex: string, segmentNames: Array<SegmentName>}} Compiled regex and segment names
|
|
294
299
|
*/
|
|
295
|
-
const compileRegex = (segments,
|
|
300
|
+
const compileRegex = (segments, options) => {
|
|
296
301
|
let regex = '^';
|
|
297
302
|
let groupIndex = 0;
|
|
298
303
|
/** @type {Array<SegmentName>} */
|
|
@@ -312,7 +317,7 @@ const compileRegex = (segments, _options) => {
|
|
|
312
317
|
|
|
313
318
|
if (seg.type === 'wildcard') {
|
|
314
319
|
optionalPart += '(.*)';
|
|
315
|
-
segmentNames.push({ name:
|
|
320
|
+
segmentNames.push({ name: options.wildcardName, index: groupIndex, type: 'wildcard' });
|
|
316
321
|
groupIndex++;
|
|
317
322
|
} else if (seg.type === 'named') {
|
|
318
323
|
optionalPart += seg.regex;
|
|
@@ -332,7 +337,7 @@ const compileRegex = (segments, _options) => {
|
|
|
332
337
|
|
|
333
338
|
if (segment.type === 'wildcard') {
|
|
334
339
|
regex += '(.*)';
|
|
335
|
-
segmentNames.push({ name:
|
|
340
|
+
segmentNames.push({ name: options.wildcardName, index: groupIndex, type: 'wildcard' });
|
|
336
341
|
groupIndex++;
|
|
337
342
|
} else if (segment.type === 'named') {
|
|
338
343
|
regex += segment.regex;
|
|
@@ -379,9 +384,20 @@ const makePattern = (pattern, options = {}) => {
|
|
|
379
384
|
* @returns {CompiledPattern} Compiled pattern
|
|
380
385
|
*/
|
|
381
386
|
const makePatternFromRegex = (regex, keys = []) => {
|
|
387
|
+
// Strip the `g` and `y` flags. The `g` flag makes `RegExp.prototype.exec`
|
|
388
|
+
// advance `lastIndex` between calls, which would cause subsequent matches
|
|
389
|
+
// to start from the wrong position. The `y` (sticky) flag requires a match
|
|
390
|
+
// starting exactly at `lastIndex`, which is incompatible with our
|
|
391
|
+
// anchored, single-match contract. Other flags (`i`, `m`, `s`, `d`, `u`)
|
|
392
|
+
// are preserved.
|
|
393
|
+
const safeFlags = regex.flags.replace(/[gy]/g, '');
|
|
394
|
+
const regexObj = safeFlags === regex.flags
|
|
395
|
+
? regex
|
|
396
|
+
: new RegExp(regex.source, safeFlags);
|
|
397
|
+
|
|
382
398
|
return {
|
|
383
399
|
regex: regex.source,
|
|
384
|
-
regexObj
|
|
400
|
+
regexObj,
|
|
385
401
|
segments: [],
|
|
386
402
|
segmentNames: keys.map((name, index) => ({ name, index, type: 'named' })),
|
|
387
403
|
options: DEFAULT_OPTIONS,
|
|
@@ -397,6 +413,14 @@ const makePatternFromRegex = (regex, keys = []) => {
|
|
|
397
413
|
* @returns {Object|null} Extracted values or null if no match
|
|
398
414
|
*/
|
|
399
415
|
const match = (compiled, str) => {
|
|
416
|
+
// Guard against non-string input. `RegExp.prototype.exec` coerces with
|
|
417
|
+
// String(), so `match(123)` would silently try to match "123" — almost
|
|
418
|
+
// certainly not what the caller meant. Throw a TypeError so the misuse
|
|
419
|
+
// surfaces immediately.
|
|
420
|
+
if (typeof str !== 'string') {
|
|
421
|
+
throw new TypeError(`pattern.match() requires a string, got ${typeof str}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
400
424
|
const matchResult = compiled.regexObj.exec(str);
|
|
401
425
|
|
|
402
426
|
if (!matchResult) {
|
|
@@ -416,6 +440,10 @@ const match = (compiled, str) => {
|
|
|
416
440
|
});
|
|
417
441
|
return result;
|
|
418
442
|
}
|
|
443
|
+
// No keys, no groups → return the captured values as an array. If the
|
|
444
|
+
// regex had no capture groups, this is an empty array; we keep the
|
|
445
|
+
// existing behaviour (documented in the README's regex example) rather
|
|
446
|
+
// than coercing to `{}` and breaking callers that rely on the array.
|
|
419
447
|
return matchResult.slice(1);
|
|
420
448
|
}
|
|
421
449
|
|
|
@@ -505,9 +533,9 @@ const stringify = (compiled, values = {}) => {
|
|
|
505
533
|
}
|
|
506
534
|
optionalPart += Array.isArray(val) ? val.join('/') : val;
|
|
507
535
|
} else if (seg.type === 'wildcard') {
|
|
508
|
-
const val = values.
|
|
509
|
-
//
|
|
510
|
-
//
|
|
536
|
+
const val = values[compiled.options.wildcardName];
|
|
537
|
+
// Wildcards inside an optional group still consume values[wildcardName]
|
|
538
|
+
// when present; if absent, the whole group is wiped.
|
|
511
539
|
if (isAbsentValue(val)) {
|
|
512
540
|
optionalPart = '';
|
|
513
541
|
break;
|
|
@@ -539,7 +567,7 @@ const stringify = (compiled, values = {}) => {
|
|
|
539
567
|
}
|
|
540
568
|
result += Array.isArray(value) ? value.join('/') : value;
|
|
541
569
|
} else if (segment.type === 'wildcard') {
|
|
542
|
-
const value = values.
|
|
570
|
+
const value = values[compiled.options.wildcardName];
|
|
543
571
|
// Empty string is a valid wildcard match result (e.g. '/files/*' on '/files/')
|
|
544
572
|
// and should be allowed without throwing.
|
|
545
573
|
if (value === undefined || value === null) {
|
|
@@ -571,6 +599,12 @@ class UrlPattern {
|
|
|
571
599
|
/** @type {CompiledPattern} */
|
|
572
600
|
this.compiled = makePattern(pattern, /** @type {UrlPatternOptions} */ (options));
|
|
573
601
|
}
|
|
602
|
+
// The compiled state is exposed as a read-only introspection field; freeze
|
|
603
|
+
// it (and the nested `options` object) so accidental mutation fails loudly
|
|
604
|
+
// instead of silently desynchronising the cached regex. `Object.freeze`
|
|
605
|
+
// is shallow, so we explicitly freeze `options` too.
|
|
606
|
+
Object.freeze(this.compiled.options);
|
|
607
|
+
Object.freeze(this.compiled);
|
|
574
608
|
}
|
|
575
609
|
|
|
576
610
|
/**
|
package/types/main.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export type UrlPatternOptions = {
|
|
6
6
|
/**
|
|
7
|
-
* - Character used for escaping
|
|
7
|
+
* - Character used for escaping. Only escapes regex metacharacters (`^$.*+?()[]{}|\`); for any other character the backslash is treated as a literal.
|
|
8
8
|
*/
|
|
9
9
|
escapeChar?: string;
|
|
10
10
|
/**
|
|
@@ -32,9 +32,13 @@ export type UrlPatternOptions = {
|
|
|
32
32
|
*/
|
|
33
33
|
optionalSegmentEndChar?: string;
|
|
34
34
|
/**
|
|
35
|
-
* - Character that denotes a wildcard
|
|
35
|
+
* - Character that denotes a wildcard in the pattern
|
|
36
36
|
*/
|
|
37
37
|
wildcardChar?: string;
|
|
38
|
+
/**
|
|
39
|
+
* - Key under which the wildcard value is stored in the match result
|
|
40
|
+
*/
|
|
41
|
+
wildcardName?: string;
|
|
38
42
|
};
|
|
39
43
|
export type ParsedSegment = {
|
|
40
44
|
/**
|
|
@@ -108,14 +112,15 @@ export type CompiledPattern = {
|
|
|
108
112
|
};
|
|
109
113
|
/**
|
|
110
114
|
* @typedef {Object} UrlPatternOptions
|
|
111
|
-
* @property {string} [escapeChar='\\'] - Character used for escaping
|
|
115
|
+
* @property {string} [escapeChar='\\'] - Character used for escaping. Only escapes regex metacharacters (`^$.*+?()[]{}|\`); for any other character the backslash is treated as a literal.
|
|
112
116
|
* @property {string} [segmentNameStartChar=':'] - Character that starts a named segment
|
|
113
117
|
* @property {string} [segmentNameEndChar] - Character that ends a named segment. When set, the segment name stops at the first occurrence of this character (instead of stopping at the first character outside `segmentNameCharset`).
|
|
114
118
|
* @property {string} [segmentNameCharset='a-zA-Z0-9_'] - Characters allowed in segment names
|
|
115
119
|
* @property {string} [segmentValueCharset='a-zA-Z0-9-_~ %'] - Characters allowed in segment values
|
|
116
120
|
* @property {string} [optionalSegmentStartChar='('] - Character that starts an optional segment
|
|
117
121
|
* @property {string} [optionalSegmentEndChar=')'] - Character that ends an optional segment
|
|
118
|
-
* @property {string} [wildcardChar='*'] - Character that denotes a wildcard
|
|
122
|
+
* @property {string} [wildcardChar='*'] - Character that denotes a wildcard in the pattern
|
|
123
|
+
* @property {string} [wildcardName='_'] - Key under which the wildcard value is stored in the match result
|
|
119
124
|
*/
|
|
120
125
|
/**
|
|
121
126
|
* @typedef {Object} ParsedSegment
|