@peter.naydenov/url-pattern 1.0.2 → 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/src/main.js CHANGED
@@ -5,13 +5,15 @@
5
5
 
6
6
  /**
7
7
  * @typedef {Object} UrlPatternOptions
8
- * @property {string} [escapeChar='\\'] - Character used for escaping special characters
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
- * @property {string} [segmentNameCharset='a-zA-Z0-9'] - Characters allowed in segment names
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
+ * @property {string} [segmentNameCharset='a-zA-Z0-9_'] - Characters allowed in segment names
11
12
  * @property {string} [segmentValueCharset='a-zA-Z0-9-_~ %'] - Characters allowed in segment values
12
13
  * @property {string} [optionalSegmentStartChar='('] - Character that starts an optional segment
13
14
  * @property {string} [optionalSegmentEndChar=')'] - Character that ends an optional segment
14
- * @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
15
17
  */
16
18
 
17
19
  /**
@@ -19,6 +21,7 @@
19
21
  * @property {string} name - Segment name
20
22
  * @property {string} type - Segment type ('named' | 'wildcard' | 'literal')
21
23
  * @property {boolean} [optional=false] - Whether the segment is optional
24
+ * @property {number} [optionalGroupId] - Identifier of the optional group this segment belongs to; absent for required segments
22
25
  * @property {string} regex - Compiled regex string
23
26
  */
24
27
 
@@ -48,11 +51,13 @@
48
51
  const DEFAULT_OPTIONS = {
49
52
  escapeChar: '\\',
50
53
  segmentNameStartChar: ':',
51
- segmentNameCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
54
+ segmentNameEndChar: undefined,
55
+ segmentNameCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_',
52
56
  segmentValueCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~ %',
53
57
  optionalSegmentStartChar: '(',
54
58
  optionalSegmentEndChar: ')',
55
- wildcardChar: '*'
59
+ wildcardChar: '*',
60
+ wildcardName: '_'
56
61
  };
57
62
 
58
63
  /**
@@ -62,6 +67,26 @@ const DEFAULT_OPTIONS = {
62
67
  */
63
68
  const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
64
69
 
70
+ /**
71
+ * Builds a safe regex character class body from a charset string.
72
+ *
73
+ * The charset is treated as a list of explicit characters — range notation
74
+ * (`a-z`) is NOT interpreted as a range. To match the same set of chars
75
+ * with range syntax, expand it (e.g. `a-z` → `abcdefghijklmnopqrstuvwxyz`).
76
+ *
77
+ * The following chars are escaped because they are special inside `[...]`:
78
+ * `\`, `]`, `^`, and `-`.
79
+ * @param {string} charset - Character class body
80
+ * @returns {string} Safe char class body
81
+ */
82
+ const escapeCharClass = (charset) => {
83
+ let escaped = charset.replace(/\\/g, '\\\\');
84
+ escaped = escaped.replace(/\]/g, '\\]');
85
+ escaped = escaped.replace(/\^/g, '\\^');
86
+ escaped = escaped.replace(/-/g, '\\-');
87
+ return escaped;
88
+ };
89
+
65
90
  /**
66
91
  * Merges default options with user provided options
67
92
  * @param {UrlPatternOptions} [userOptions={}] - User provided options
@@ -112,29 +137,63 @@ const parsePattern = (pattern, options) => {
112
137
  const segments = [];
113
138
  let i = 0;
114
139
  let inOptional = false;
140
+ let optionalGroupId = 0;
141
+ let parenDepth = 0;
115
142
 
116
143
  while (i < pattern.length) {
117
144
  const char = pattern[i];
118
145
 
119
- if (char === options.escapeChar && i + 1 < pattern.length) {
120
- segments.push({
121
- type: 'literal',
122
- name: pattern[i + 1],
123
- regex: escapeRegex(pattern[i + 1]),
124
- optional: inOptional
125
- });
126
- i += 2;
127
- continue;
146
+ if (char === options.escapeChar) {
147
+ // Throw when the escape char is at the end of the pattern — nothing follows
148
+ // it to escape, and the resulting regex would be broken.
149
+ if (i + 1 >= pattern.length) {
150
+ throw new Error(`Invalid pattern: '\\' at position ${i} has nothing to escape`);
151
+ }
152
+ const nextChar = pattern[i + 1];
153
+ // Only treat \ as an escape when followed by a regex metacharacter.
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 = '^$.*+?()[]{}|\\';
158
+ if (!regexMetachars.includes(nextChar)) {
159
+ // Not a regex metachar: treat the backslash as a literal character,
160
+ // advance past it so the next character is processed normally.
161
+ segments.push({
162
+ type: 'literal',
163
+ name: '\\',
164
+ regex: '\\\\',
165
+ optional: inOptional,
166
+ optionalGroupId: inOptional ? optionalGroupId : undefined
167
+ });
168
+ i += 1;
169
+ continue;
170
+ } else {
171
+ segments.push({
172
+ type: 'literal',
173
+ name: nextChar,
174
+ regex: escapeRegex(nextChar),
175
+ optional: inOptional,
176
+ optionalGroupId: inOptional ? optionalGroupId : undefined
177
+ });
178
+ i += 2;
179
+ continue;
180
+ }
128
181
  }
129
182
 
130
183
  if (char === options.optionalSegmentStartChar) {
131
184
  inOptional = true;
185
+ optionalGroupId++;
186
+ parenDepth++;
132
187
  i++;
133
188
  continue;
134
189
  }
135
190
 
136
191
  if (char === options.optionalSegmentEndChar) {
192
+ if (parenDepth === 0) {
193
+ throw new Error(`Invalid pattern: unmatched '${char}' at position ${i}`);
194
+ }
137
195
  inOptional = false;
196
+ parenDepth--;
138
197
  i++;
139
198
  continue;
140
199
  }
@@ -142,9 +201,10 @@ const parsePattern = (pattern, options) => {
142
201
  if (char === options.wildcardChar) {
143
202
  segments.push({
144
203
  type: 'wildcard',
145
- name: '_',
204
+ name: options.wildcardName,
146
205
  regex: '.*',
147
- optional: inOptional
206
+ optional: inOptional,
207
+ optionalGroupId: inOptional ? optionalGroupId : undefined
148
208
  });
149
209
  i++;
150
210
  continue;
@@ -154,8 +214,12 @@ const parsePattern = (pattern, options) => {
154
214
  const remaining = pattern.slice(i + 1);
155
215
  let nameEnd = 0;
156
216
  const charset = options.segmentNameCharset || '';
157
-
217
+ const endChar = options.segmentNameEndChar;
218
+
158
219
  for (let j = 0; j < remaining.length; j++) {
220
+ if (endChar && remaining[j] === endChar) {
221
+ break;
222
+ }
159
223
  if (!charset.includes(remaining[j])) {
160
224
  break;
161
225
  }
@@ -163,54 +227,77 @@ const parsePattern = (pattern, options) => {
163
227
  }
164
228
 
165
229
  const name = remaining.slice(0, nameEnd);
166
-
230
+ const consumeEndChar = !!(endChar && remaining[nameEnd] === endChar);
231
+
167
232
  if (name.length > 0) {
168
- let valueCharset = options.segmentValueCharset || '';
169
- if (valueCharset.includes('-') && valueCharset.indexOf('-') > 0 && valueCharset.indexOf('-') < valueCharset.length - 1) {
170
- valueCharset = valueCharset.replace(/-/g, '');
171
- valueCharset += '-';
172
- }
173
- const escapedValueCharset = valueCharset.replace(/\]/g, '\\]');
233
+ const valueCharset = options.segmentValueCharset || '';
234
+ const escapedValueCharset = escapeCharClass(valueCharset);
174
235
  const valueRegex = `([${escapedValueCharset}]+)`;
175
-
236
+
176
237
  segments.push({
177
238
  type: 'named',
178
239
  name,
179
240
  regex: valueRegex,
180
- optional: inOptional
241
+ optional: inOptional,
242
+ optionalGroupId: inOptional ? optionalGroupId : undefined
181
243
  });
182
- i += 1 + nameEnd;
244
+ i += 1 + nameEnd + (consumeEndChar ? 1 : 0);
183
245
  continue;
184
246
  }
247
+
248
+ // segmentNameStartChar at end of pattern, or followed by a non-charset
249
+ // character that is also a special char (e.g. ':)').
250
+ throw new Error(`Invalid pattern: '${char}' at position ${i} has no segment name`);
185
251
  }
186
252
 
187
253
  const literalEnd = findNextSpecialChar(pattern, i, options);
188
-
254
+
189
255
  if (literalEnd > i) {
190
256
  const literal = pattern.slice(i, literalEnd);
191
257
  segments.push({
192
258
  type: 'literal',
193
259
  name: literal,
194
260
  regex: escapeRegex(literal),
195
- optional: inOptional
261
+ optional: inOptional,
262
+ optionalGroupId: inOptional ? optionalGroupId : undefined
196
263
  });
197
264
  i = literalEnd;
198
265
  continue;
199
266
  }
200
267
 
201
- i++;
268
+ // literalEnd === i: current char itself is a special char with no
269
+ // name/value following. Throw for clarity.
270
+ throw new Error(`Invalid pattern: '${char}' at position ${i} has no segment name`);
271
+ }
272
+
273
+ if (parenDepth !== 0) {
274
+ throw new Error(`Invalid pattern: unclosed '${options.optionalSegmentStartChar}'`);
202
275
  }
203
276
 
204
277
  return segments;
205
278
  };
206
279
 
280
+ /**
281
+ * Returns true when a value should be treated as "absent" for an optional segment.
282
+ * @param {*} val - Value to inspect
283
+ * @returns {boolean}
284
+ */
285
+ const isAbsentValue = (val) => {
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;
290
+ if (Array.isArray(val) && val.length === 0) return true;
291
+ return false;
292
+ };
293
+
207
294
  /**
208
295
  * Compiles segments into a regex pattern
209
296
  * @param {Array<ParsedSegment>} segments - Parsed segments
210
297
  * @param {UrlPatternOptions} options - Options
211
298
  * @returns {{regex: string, segmentNames: Array<SegmentName>}} Compiled regex and segment names
212
299
  */
213
- const compileRegex = (segments, _options) => {
300
+ const compileRegex = (segments, options) => {
214
301
  let regex = '^';
215
302
  let groupIndex = 0;
216
303
  /** @type {Array<SegmentName>} */
@@ -223,13 +310,14 @@ const compileRegex = (segments, _options) => {
223
310
  if (segment.optional) {
224
311
  let optionalPart = '';
225
312
  let j = i;
226
-
227
- while (j < segments.length && segments[j].optional) {
313
+ const currentGroupId = segment.optionalGroupId;
314
+
315
+ while (j < segments.length && segments[j].optional && segments[j].optionalGroupId === currentGroupId) {
228
316
  const seg = segments[j];
229
317
 
230
318
  if (seg.type === 'wildcard') {
231
319
  optionalPart += '(.*)';
232
- segmentNames.push({ name: '_', index: groupIndex, type: 'wildcard' });
320
+ segmentNames.push({ name: options.wildcardName, index: groupIndex, type: 'wildcard' });
233
321
  groupIndex++;
234
322
  } else if (seg.type === 'named') {
235
323
  optionalPart += seg.regex;
@@ -249,7 +337,7 @@ const compileRegex = (segments, _options) => {
249
337
 
250
338
  if (segment.type === 'wildcard') {
251
339
  regex += '(.*)';
252
- segmentNames.push({ name: '_', index: groupIndex, type: 'wildcard' });
340
+ segmentNames.push({ name: options.wildcardName, index: groupIndex, type: 'wildcard' });
253
341
  groupIndex++;
254
342
  } else if (segment.type === 'named') {
255
343
  regex += segment.regex;
@@ -296,9 +384,20 @@ const makePattern = (pattern, options = {}) => {
296
384
  * @returns {CompiledPattern} Compiled pattern
297
385
  */
298
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
+
299
398
  return {
300
399
  regex: regex.source,
301
- regexObj: regex,
400
+ regexObj,
302
401
  segments: [],
303
402
  segmentNames: keys.map((name, index) => ({ name, index, type: 'named' })),
304
403
  options: DEFAULT_OPTIONS,
@@ -314,6 +413,14 @@ const makePatternFromRegex = (regex, keys = []) => {
314
413
  * @returns {Object|null} Extracted values or null if no match
315
414
  */
316
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
+
317
424
  const matchResult = compiled.regexObj.exec(str);
318
425
 
319
426
  if (!matchResult) {
@@ -325,20 +432,40 @@ const match = (compiled, str) => {
325
432
  const result = {};
326
433
  compiled.keys.forEach((key, index) => {
327
434
  const val = matchResult[index + 1];
328
- result[key] = val !== undefined ? val : null;
435
+ // Undefined means the optional group didn't participate — omit the key
436
+ // so this is consistent with string-pattern behaviour.
437
+ if (val !== undefined) {
438
+ result[key] = val;
439
+ }
329
440
  });
330
441
  return result;
331
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.
332
447
  return matchResult.slice(1);
333
448
  }
334
449
 
335
450
  const result = {};
336
451
  const usedNames = new Set();
337
452
 
453
+ // Track which segment names are wildcards so their empty-string captures survive
454
+ // the cleanup loop. An empty wildcard result (e.g. '/files/*' on '/files/')
455
+ // is a valid match result.
456
+ const wildcardNames = new Set();
457
+ for (const seg of compiled.segmentNames) {
458
+ if (seg.type === 'wildcard') wildcardNames.add(seg.name);
459
+ }
460
+
338
461
  for (let i = 0; i < compiled.segmentNames.length; i++) {
339
462
  const segInfo = compiled.segmentNames[i];
340
- const value = matchResult[segInfo.index + 1] || '';
341
-
463
+ const hasCapture = segInfo.index + 1 in matchResult;
464
+ // When an optional group didn't participate, the regex returns undefined.
465
+ // Normalize to '' so both non-isRegex and isRegex paths behave the same.
466
+ const rawValue = hasCapture ? matchResult[segInfo.index + 1] : '';
467
+ const value = rawValue === undefined ? '' : rawValue;
468
+
342
469
  if (usedNames.has(segInfo.name)) {
343
470
  if (!Array.isArray(result[segInfo.name])) {
344
471
  result[segInfo.name] = [result[segInfo.name]];
@@ -350,8 +477,19 @@ const match = (compiled, str) => {
350
477
  }
351
478
  }
352
479
 
480
+ // Delete empty-string entries, but preserve wildcards — an empty capture is
481
+ // a valid match result for wildcards. Also clean null/undefined from arrays.
353
482
  for (const key in result) {
354
- if (result[key] === '') {
483
+ const val = result[key];
484
+ if (Array.isArray(val)) {
485
+ // Filter out empty-string entries from arrays (but preserve wildcards).
486
+ const filtered = val.filter(v => v !== '' || wildcardNames.has(key));
487
+ if (filtered.length === 0) {
488
+ delete result[key];
489
+ } else {
490
+ result[key] = filtered;
491
+ }
492
+ } else if (val === '' && !wildcardNames.has(key)) {
355
493
  delete result[key];
356
494
  }
357
495
  }
@@ -380,37 +518,38 @@ const stringify = (compiled, values = {}) => {
380
518
  if (segment.optional) {
381
519
  let optionalPart = '';
382
520
  let j = i;
383
-
384
- while (j < compiled.segments.length && compiled.segments[j].optional) {
521
+ const currentGroupId = segment.optionalGroupId;
522
+
523
+ while (j < compiled.segments.length && compiled.segments[j].optional && compiled.segments[j].optionalGroupId === currentGroupId) {
385
524
  const seg = compiled.segments[j];
386
-
525
+
387
526
  if (seg.type === 'literal') {
388
527
  optionalPart += seg.name;
389
528
  } else if (seg.type === 'named') {
390
529
  const val = values[seg.name];
391
- if (val !== undefined && val !== null && val !== '') {
392
- optionalPart += Array.isArray(val) ? val.join('/') : val;
393
- } else {
530
+ if (isAbsentValue(val)) {
394
531
  optionalPart = '';
395
532
  break;
396
533
  }
534
+ optionalPart += Array.isArray(val) ? val.join('/') : val;
397
535
  } else if (seg.type === 'wildcard') {
398
- const val = values._;
399
- if (val !== undefined && val !== null && val !== '') {
400
- optionalPart += Array.isArray(val) ? val.join('/') : val;
401
- } else {
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.
539
+ if (isAbsentValue(val)) {
402
540
  optionalPart = '';
403
541
  break;
404
542
  }
543
+ optionalPart += Array.isArray(val) ? val.join('/') : val;
405
544
  }
406
-
545
+
407
546
  j++;
408
547
  }
409
-
548
+
410
549
  if (optionalPart !== '') {
411
550
  result += optionalPart;
412
551
  }
413
-
552
+
414
553
  if (i === j) {
415
554
  i++;
416
555
  } else {
@@ -423,13 +562,15 @@ const stringify = (compiled, values = {}) => {
423
562
  result += segment.name;
424
563
  } else if (segment.type === 'named') {
425
564
  const value = values[segment.name];
426
- if (value === undefined || value === null || value === '') {
565
+ if (isAbsentValue(value)) {
427
566
  throw new Error(`Missing required value for segment: ${segment.name}`);
428
567
  }
429
568
  result += Array.isArray(value) ? value.join('/') : value;
430
569
  } else if (segment.type === 'wildcard') {
431
- const value = values._;
432
- if (value === undefined || value === null || value === '') {
570
+ const value = values[compiled.options.wildcardName];
571
+ // Empty string is a valid wildcard match result (e.g. '/files/*' on '/files/')
572
+ // and should be allowed without throwing.
573
+ if (value === undefined || value === null) {
433
574
  throw new Error('Missing required wildcard value');
434
575
  }
435
576
  result += Array.isArray(value) ? value.join('/') : value;
@@ -458,6 +599,12 @@ class UrlPattern {
458
599
  /** @type {CompiledPattern} */
459
600
  this.compiled = makePattern(pattern, /** @type {UrlPatternOptions} */ (options));
460
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);
461
608
  }
462
609
 
463
610
  /**
@@ -490,4 +637,4 @@ const urlPattern = (pattern, options = {}) => {
490
637
  };
491
638
 
492
639
  export { UrlPattern, urlPattern, makePattern, makePatternFromRegex, match, stringify, DEFAULT_OPTIONS };
493
- export default UrlPattern;
640
+ export default urlPattern;
package/types/main.d.ts CHANGED
@@ -1,13 +1,20 @@
1
- export default UrlPattern;
1
+ /**
2
+ * @fileoverview URL pattern matching library
3
+ * @module url-pattern
4
+ */
2
5
  export type UrlPatternOptions = {
3
6
  /**
4
- * - Character used for escaping special characters
7
+ * - Character used for escaping. Only escapes regex metacharacters (`^$.*+?()[]{}|\`); for any other character the backslash is treated as a literal.
5
8
  */
6
9
  escapeChar?: string;
7
10
  /**
8
11
  * - Character that starts a named segment
9
12
  */
10
13
  segmentNameStartChar?: string;
14
+ /**
15
+ * - 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`).
16
+ */
17
+ segmentNameEndChar?: string;
11
18
  /**
12
19
  * - Characters allowed in segment names
13
20
  */
@@ -25,9 +32,13 @@ export type UrlPatternOptions = {
25
32
  */
26
33
  optionalSegmentEndChar?: string;
27
34
  /**
28
- * - Character that denotes a wildcard
35
+ * - Character that denotes a wildcard in the pattern
29
36
  */
30
37
  wildcardChar?: string;
38
+ /**
39
+ * - Key under which the wildcard value is stored in the match result
40
+ */
41
+ wildcardName?: string;
31
42
  };
32
43
  export type ParsedSegment = {
33
44
  /**
@@ -42,6 +53,10 @@ export type ParsedSegment = {
42
53
  * - Whether the segment is optional
43
54
  */
44
55
  optional?: boolean;
56
+ /**
57
+ * - Identifier of the optional group this segment belongs to; absent for required segments
58
+ */
59
+ optionalGroupId?: number;
45
60
  /**
46
61
  * - Compiled regex string
47
62
  */
@@ -95,85 +110,24 @@ export type CompiledPattern = {
95
110
  */
96
111
  keys?: Array<string>;
97
112
  };
98
- /**
99
- * UrlPattern class for matching and generating URLs
100
- */
101
- export class UrlPattern {
102
- /**
103
- * @param {string|RegExp} pattern - Pattern string or regex
104
- * @param {UrlPatternOptions|Array<string>} [options={}] - Options or keys (for regex)
105
- */
106
- constructor(pattern: string | RegExp, options?: UrlPatternOptions | Array<string>);
107
- /** @type {CompiledPattern} */
108
- compiled: CompiledPattern;
109
- /**
110
- * Match a string against the pattern
111
- * @param {string} str - String to match
112
- * @returns {Object|null} Extracted values or null if no match
113
- */
114
- match(str: string): any | null;
115
- /**
116
- * Generate a string from the pattern
117
- * @param {Object} [values={}] - Values to stringify
118
- * @returns {string} Generated string
119
- */
120
- stringify(values?: any): string;
121
- }
122
- /**
123
- * Creates a new UrlPattern instance (functional API)
124
- * @param {string|RegExp} pattern - Pattern string or regex
125
- * @param {UrlPatternOptions|Array<string>} [options={}] - Options or keys
126
- * @returns {UrlPattern} UrlPattern instance
127
- */
128
- export function urlPattern(pattern: string | RegExp, options?: UrlPatternOptions | Array<string>): UrlPattern;
129
- /**
130
- * Creates a compiled pattern from a string
131
- * @param {string} pattern - Pattern string
132
- * @param {UrlPatternOptions} [options={}] - Options
133
- * @returns {CompiledPattern} Compiled pattern
134
- */
135
- export function makePattern(pattern: string, options?: UrlPatternOptions): CompiledPattern;
136
- /**
137
- * Creates a compiled pattern from a regex
138
- * @param {RegExp} regex - Regex pattern
139
- * @param {Array<string>} [keys=[]] - Array of key names for captured groups
140
- * @returns {CompiledPattern} Compiled pattern
141
- */
142
- export function makePatternFromRegex(regex: RegExp, keys?: Array<string>): CompiledPattern;
143
- /**
144
- * Matches a string against a compiled pattern
145
- * @param {CompiledPattern} compiled - Compiled pattern
146
- * @param {string} str - String to match
147
- * @returns {Object|null} Extracted values or null if no match
148
- */
149
- export function match(compiled: CompiledPattern, str: string): any | null;
150
- /**
151
- * Stringifies a pattern with given values
152
- * @param {CompiledPattern} compiled - Compiled pattern
153
- * @param {Object} [values={}] - Values to stringify
154
- * @returns {string} Generated string
155
- * @throws {Error} If required values are missing
156
- */
157
- export function stringify(compiled: CompiledPattern, values?: any): string;
158
- /**
159
- * @fileoverview URL pattern matching library
160
- * @module url-pattern
161
- */
162
113
  /**
163
114
  * @typedef {Object} UrlPatternOptions
164
- * @property {string} [escapeChar='\\'] - Character used for escaping special characters
115
+ * @property {string} [escapeChar='\\'] - Character used for escaping. Only escapes regex metacharacters (`^$.*+?()[]{}|\`); for any other character the backslash is treated as a literal.
165
116
  * @property {string} [segmentNameStartChar=':'] - Character that starts a named segment
166
- * @property {string} [segmentNameCharset='a-zA-Z0-9'] - Characters allowed in segment names
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`).
118
+ * @property {string} [segmentNameCharset='a-zA-Z0-9_'] - Characters allowed in segment names
167
119
  * @property {string} [segmentValueCharset='a-zA-Z0-9-_~ %'] - Characters allowed in segment values
168
120
  * @property {string} [optionalSegmentStartChar='('] - Character that starts an optional segment
169
121
  * @property {string} [optionalSegmentEndChar=')'] - Character that ends an optional segment
170
- * @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
171
124
  */
172
125
  /**
173
126
  * @typedef {Object} ParsedSegment
174
127
  * @property {string} name - Segment name
175
128
  * @property {string} type - Segment type ('named' | 'wildcard' | 'literal')
176
129
  * @property {boolean} [optional=false] - Whether the segment is optional
130
+ * @property {number} [optionalGroupId] - Identifier of the optional group this segment belongs to; absent for required segments
177
131
  * @property {string} regex - Compiled regex string
178
132
  */
179
133
  /**
@@ -197,4 +151,66 @@ export function stringify(compiled: CompiledPattern, values?: any): string;
197
151
  * Default options for URL pattern matching
198
152
  * @type {UrlPatternOptions}
199
153
  */
200
- export const DEFAULT_OPTIONS: UrlPatternOptions;
154
+ declare const DEFAULT_OPTIONS: UrlPatternOptions;
155
+ /**
156
+ * Creates a compiled pattern from a string
157
+ * @param {string} pattern - Pattern string
158
+ * @param {UrlPatternOptions} [options={}] - Options
159
+ * @returns {CompiledPattern} Compiled pattern
160
+ */
161
+ declare const makePattern: (pattern: string, options?: UrlPatternOptions) => CompiledPattern;
162
+ /**
163
+ * Creates a compiled pattern from a regex
164
+ * @param {RegExp} regex - Regex pattern
165
+ * @param {Array<string>} [keys=[]] - Array of key names for captured groups
166
+ * @returns {CompiledPattern} Compiled pattern
167
+ */
168
+ declare const makePatternFromRegex: (regex: RegExp, keys?: Array<string>) => CompiledPattern;
169
+ /**
170
+ * Matches a string against a compiled pattern
171
+ * @param {CompiledPattern} compiled - Compiled pattern
172
+ * @param {string} str - String to match
173
+ * @returns {Object|null} Extracted values or null if no match
174
+ */
175
+ declare const match: (compiled: CompiledPattern, str: string) => any | null;
176
+ /**
177
+ * Stringifies a pattern with given values
178
+ * @param {CompiledPattern} compiled - Compiled pattern
179
+ * @param {Object} [values={}] - Values to stringify
180
+ * @returns {string} Generated string
181
+ * @throws {Error} If required values are missing
182
+ */
183
+ declare const stringify: (compiled: CompiledPattern, values?: any) => string;
184
+ /**
185
+ * UrlPattern class for matching and generating URLs
186
+ */
187
+ declare class UrlPattern {
188
+ /** @type {CompiledPattern} */
189
+ compiled: CompiledPattern;
190
+ /**
191
+ * @param {string|RegExp} pattern - Pattern string or regex
192
+ * @param {UrlPatternOptions|Array<string>} [options={}] - Options or keys (for regex)
193
+ */
194
+ constructor(pattern: string | RegExp, options?: UrlPatternOptions | Array<string>);
195
+ /**
196
+ * Match a string against the pattern
197
+ * @param {string} str - String to match
198
+ * @returns {Object|null} Extracted values or null if no match
199
+ */
200
+ match(str: string): any | null;
201
+ /**
202
+ * Generate a string from the pattern
203
+ * @param {Object} [values={}] - Values to stringify
204
+ * @returns {string} Generated string
205
+ */
206
+ stringify(values?: any): string;
207
+ }
208
+ /**
209
+ * Creates a new UrlPattern instance (functional API)
210
+ * @param {string|RegExp} pattern - Pattern string or regex
211
+ * @param {UrlPatternOptions|Array<string>} [options={}] - Options or keys
212
+ * @returns {UrlPattern} UrlPattern instance
213
+ */
214
+ declare const urlPattern: (pattern: string | RegExp, options?: UrlPatternOptions | Array<string>) => UrlPattern;
215
+ export { UrlPattern, urlPattern, makePattern, makePatternFromRegex, match, stringify, DEFAULT_OPTIONS };
216
+ export default urlPattern;