@peter.naydenov/url-pattern 1.0.2 → 1.0.3
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 +162 -47
- package/dist/url-pattern.es.js +160 -47
- package/dist/url-pattern.umd.js +1 -1
- package/package.json +6 -6
- package/{README.md → readme.md} +6 -6
- package/src/main.js +160 -47
- package/types/main.d.ts +78 -67
package/dist/url-pattern.es.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* @typedef {Object} UrlPatternOptions
|
|
8
8
|
* @property {string} [escapeChar='\\'] - Character used for escaping special characters
|
|
9
9
|
* @property {string} [segmentNameStartChar=':'] - Character that starts a named segment
|
|
10
|
-
* @property {string} [
|
|
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
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
* @property {string} name - Segment name
|
|
20
21
|
* @property {string} type - Segment type ('named' | 'wildcard' | 'literal')
|
|
21
22
|
* @property {boolean} [optional=false] - Whether the segment is optional
|
|
23
|
+
* @property {number} [optionalGroupId] - Identifier of the optional group this segment belongs to; absent for required segments
|
|
22
24
|
* @property {string} regex - Compiled regex string
|
|
23
25
|
*/
|
|
24
26
|
|
|
@@ -48,7 +50,8 @@
|
|
|
48
50
|
const DEFAULT_OPTIONS = {
|
|
49
51
|
escapeChar: '\\',
|
|
50
52
|
segmentNameStartChar: ':',
|
|
51
|
-
|
|
53
|
+
segmentNameEndChar: undefined,
|
|
54
|
+
segmentNameCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_',
|
|
52
55
|
segmentValueCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~ %',
|
|
53
56
|
optionalSegmentStartChar: '(',
|
|
54
57
|
optionalSegmentEndChar: ')',
|
|
@@ -62,6 +65,26 @@ const DEFAULT_OPTIONS = {
|
|
|
62
65
|
*/
|
|
63
66
|
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
64
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Builds a safe regex character class body from a charset string.
|
|
70
|
+
*
|
|
71
|
+
* The charset is treated as a list of explicit characters — range notation
|
|
72
|
+
* (`a-z`) is NOT interpreted as a range. To match the same set of chars
|
|
73
|
+
* with range syntax, expand it (e.g. `a-z` → `abcdefghijklmnopqrstuvwxyz`).
|
|
74
|
+
*
|
|
75
|
+
* The following chars are escaped because they are special inside `[...]`:
|
|
76
|
+
* `\`, `]`, `^`, and `-`.
|
|
77
|
+
* @param {string} charset - Character class body
|
|
78
|
+
* @returns {string} Safe char class body
|
|
79
|
+
*/
|
|
80
|
+
const escapeCharClass = (charset) => {
|
|
81
|
+
let escaped = charset.replace(/\\/g, '\\\\');
|
|
82
|
+
escaped = escaped.replace(/\]/g, '\\]');
|
|
83
|
+
escaped = escaped.replace(/\^/g, '\\^');
|
|
84
|
+
escaped = escaped.replace(/-/g, '\\-');
|
|
85
|
+
return escaped;
|
|
86
|
+
};
|
|
87
|
+
|
|
65
88
|
/**
|
|
66
89
|
* Merges default options with user provided options
|
|
67
90
|
* @param {UrlPatternOptions} [userOptions={}] - User provided options
|
|
@@ -112,29 +135,62 @@ const parsePattern = (pattern, options) => {
|
|
|
112
135
|
const segments = [];
|
|
113
136
|
let i = 0;
|
|
114
137
|
let inOptional = false;
|
|
138
|
+
let optionalGroupId = 0;
|
|
139
|
+
let parenDepth = 0;
|
|
115
140
|
|
|
116
141
|
while (i < pattern.length) {
|
|
117
142
|
const char = pattern[i];
|
|
118
143
|
|
|
119
|
-
if (char === options.escapeChar
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
144
|
+
if (char === options.escapeChar) {
|
|
145
|
+
// Throw when the escape char is at the end of the pattern — nothing follows
|
|
146
|
+
// it to escape, and the resulting regex would be broken.
|
|
147
|
+
if (i + 1 >= pattern.length) {
|
|
148
|
+
throw new Error(`Invalid pattern: '\\' at position ${i} has nothing to escape`);
|
|
149
|
+
}
|
|
150
|
+
const nextChar = pattern[i + 1];
|
|
151
|
+
// Only treat \ as an escape when followed by a regex metacharacter.
|
|
152
|
+
// For all other characters the backslash is literal (e.g. '\)' in a URL
|
|
153
|
+
// is just a backslash followed by ')'). This prevents the escape handler
|
|
154
|
+
// from accidentally consuming the optional-group close delimiter ')'.
|
|
155
|
+
const regexMetachars = '^$\.*+?()[]{}|\\';
|
|
156
|
+
if (!regexMetachars.includes(nextChar)) {
|
|
157
|
+
// Not a regex metachar: treat the backslash as a literal character,
|
|
158
|
+
// advance past it so the next character is processed normally.
|
|
159
|
+
segments.push({
|
|
160
|
+
type: 'literal',
|
|
161
|
+
name: '\\',
|
|
162
|
+
regex: '\\\\',
|
|
163
|
+
optional: inOptional,
|
|
164
|
+
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
165
|
+
});
|
|
166
|
+
i += 1;
|
|
167
|
+
} else {
|
|
168
|
+
segments.push({
|
|
169
|
+
type: 'literal',
|
|
170
|
+
name: nextChar,
|
|
171
|
+
regex: escapeRegex(nextChar),
|
|
172
|
+
optional: inOptional,
|
|
173
|
+
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
174
|
+
});
|
|
175
|
+
i += 2;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
128
178
|
}
|
|
129
179
|
|
|
130
180
|
if (char === options.optionalSegmentStartChar) {
|
|
131
181
|
inOptional = true;
|
|
182
|
+
optionalGroupId++;
|
|
183
|
+
parenDepth++;
|
|
132
184
|
i++;
|
|
133
185
|
continue;
|
|
134
186
|
}
|
|
135
187
|
|
|
136
188
|
if (char === options.optionalSegmentEndChar) {
|
|
189
|
+
if (parenDepth === 0) {
|
|
190
|
+
throw new Error(`Invalid pattern: unmatched '${char}' at position ${i}`);
|
|
191
|
+
}
|
|
137
192
|
inOptional = false;
|
|
193
|
+
parenDepth--;
|
|
138
194
|
i++;
|
|
139
195
|
continue;
|
|
140
196
|
}
|
|
@@ -144,7 +200,8 @@ const parsePattern = (pattern, options) => {
|
|
|
144
200
|
type: 'wildcard',
|
|
145
201
|
name: '_',
|
|
146
202
|
regex: '.*',
|
|
147
|
-
optional: inOptional
|
|
203
|
+
optional: inOptional,
|
|
204
|
+
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
148
205
|
});
|
|
149
206
|
i++;
|
|
150
207
|
continue;
|
|
@@ -154,8 +211,12 @@ const parsePattern = (pattern, options) => {
|
|
|
154
211
|
const remaining = pattern.slice(i + 1);
|
|
155
212
|
let nameEnd = 0;
|
|
156
213
|
const charset = options.segmentNameCharset || '';
|
|
157
|
-
|
|
214
|
+
const endChar = options.segmentNameEndChar;
|
|
215
|
+
|
|
158
216
|
for (let j = 0; j < remaining.length; j++) {
|
|
217
|
+
if (endChar && remaining[j] === endChar) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
159
220
|
if (!charset.includes(remaining[j])) {
|
|
160
221
|
break;
|
|
161
222
|
}
|
|
@@ -163,47 +224,68 @@ const parsePattern = (pattern, options) => {
|
|
|
163
224
|
}
|
|
164
225
|
|
|
165
226
|
const name = remaining.slice(0, nameEnd);
|
|
166
|
-
|
|
227
|
+
const consumeEndChar = !!(endChar && remaining[nameEnd] === endChar);
|
|
228
|
+
|
|
167
229
|
if (name.length > 0) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
valueCharset = valueCharset.replace(/-/g, '');
|
|
171
|
-
valueCharset += '-';
|
|
172
|
-
}
|
|
173
|
-
const escapedValueCharset = valueCharset.replace(/\]/g, '\\]');
|
|
230
|
+
const valueCharset = options.segmentValueCharset || '';
|
|
231
|
+
const escapedValueCharset = escapeCharClass(valueCharset);
|
|
174
232
|
const valueRegex = `([${escapedValueCharset}]+)`;
|
|
175
|
-
|
|
233
|
+
|
|
176
234
|
segments.push({
|
|
177
235
|
type: 'named',
|
|
178
236
|
name,
|
|
179
237
|
regex: valueRegex,
|
|
180
|
-
optional: inOptional
|
|
238
|
+
optional: inOptional,
|
|
239
|
+
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
181
240
|
});
|
|
182
|
-
i += 1 + nameEnd;
|
|
241
|
+
i += 1 + nameEnd + (consumeEndChar ? 1 : 0);
|
|
183
242
|
continue;
|
|
184
243
|
}
|
|
244
|
+
|
|
245
|
+
// segmentNameStartChar at end of pattern, or followed by a non-charset
|
|
246
|
+
// character that is also a special char (e.g. ':)').
|
|
247
|
+
throw new Error(`Invalid pattern: '${char}' at position ${i} has no segment name`);
|
|
185
248
|
}
|
|
186
249
|
|
|
187
250
|
const literalEnd = findNextSpecialChar(pattern, i, options);
|
|
188
|
-
|
|
251
|
+
|
|
189
252
|
if (literalEnd > i) {
|
|
190
253
|
const literal = pattern.slice(i, literalEnd);
|
|
191
254
|
segments.push({
|
|
192
255
|
type: 'literal',
|
|
193
256
|
name: literal,
|
|
194
257
|
regex: escapeRegex(literal),
|
|
195
|
-
optional: inOptional
|
|
258
|
+
optional: inOptional,
|
|
259
|
+
optionalGroupId: inOptional ? optionalGroupId : undefined
|
|
196
260
|
});
|
|
197
261
|
i = literalEnd;
|
|
198
262
|
continue;
|
|
199
263
|
}
|
|
200
264
|
|
|
201
|
-
i
|
|
265
|
+
// literalEnd === i: current char itself is a special char with no
|
|
266
|
+
// name/value following. Throw for clarity.
|
|
267
|
+
throw new Error(`Invalid pattern: '${char}' at position ${i} has no segment name`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (parenDepth !== 0) {
|
|
271
|
+
throw new Error(`Invalid pattern: unclosed '${options.optionalSegmentStartChar}'`);
|
|
202
272
|
}
|
|
203
273
|
|
|
204
274
|
return segments;
|
|
205
275
|
};
|
|
206
276
|
|
|
277
|
+
/**
|
|
278
|
+
/**
|
|
279
|
+
* Returns true when a value should be treated as "absent" for an optional segment.
|
|
280
|
+
* @param {*} val - Value to inspect
|
|
281
|
+
* @returns {boolean}
|
|
282
|
+
*/
|
|
283
|
+
const isAbsentValue = (val) => {
|
|
284
|
+
if (val === undefined || val === null || val === '') return true;
|
|
285
|
+
if (Array.isArray(val) && val.length === 0) return true;
|
|
286
|
+
return false;
|
|
287
|
+
};
|
|
288
|
+
|
|
207
289
|
/**
|
|
208
290
|
* Compiles segments into a regex pattern
|
|
209
291
|
* @param {Array<ParsedSegment>} segments - Parsed segments
|
|
@@ -223,8 +305,9 @@ const compileRegex = (segments, _options) => {
|
|
|
223
305
|
if (segment.optional) {
|
|
224
306
|
let optionalPart = '';
|
|
225
307
|
let j = i;
|
|
226
|
-
|
|
227
|
-
|
|
308
|
+
const currentGroupId = segment.optionalGroupId;
|
|
309
|
+
|
|
310
|
+
while (j < segments.length && segments[j].optional && segments[j].optionalGroupId === currentGroupId) {
|
|
228
311
|
const seg = segments[j];
|
|
229
312
|
|
|
230
313
|
if (seg.type === 'wildcard') {
|
|
@@ -325,7 +408,11 @@ const match = (compiled, str) => {
|
|
|
325
408
|
const result = {};
|
|
326
409
|
compiled.keys.forEach((key, index) => {
|
|
327
410
|
const val = matchResult[index + 1];
|
|
328
|
-
|
|
411
|
+
// Undefined means the optional group didn't participate — omit the key
|
|
412
|
+
// so this is consistent with string-pattern behaviour.
|
|
413
|
+
if (val !== undefined) {
|
|
414
|
+
result[key] = val;
|
|
415
|
+
}
|
|
329
416
|
});
|
|
330
417
|
return result;
|
|
331
418
|
}
|
|
@@ -335,10 +422,22 @@ const match = (compiled, str) => {
|
|
|
335
422
|
const result = {};
|
|
336
423
|
const usedNames = new Set();
|
|
337
424
|
|
|
425
|
+
// Track which segment names are wildcards so their empty-string captures survive
|
|
426
|
+
// the cleanup loop. An empty wildcard result (e.g. '/files/*' on '/files/')
|
|
427
|
+
// is a valid match result.
|
|
428
|
+
const wildcardNames = new Set();
|
|
429
|
+
for (const seg of compiled.segmentNames) {
|
|
430
|
+
if (seg.type === 'wildcard') wildcardNames.add(seg.name);
|
|
431
|
+
}
|
|
432
|
+
|
|
338
433
|
for (let i = 0; i < compiled.segmentNames.length; i++) {
|
|
339
434
|
const segInfo = compiled.segmentNames[i];
|
|
340
|
-
const
|
|
341
|
-
|
|
435
|
+
const hasCapture = segInfo.index + 1 in matchResult;
|
|
436
|
+
// When an optional group didn't participate, the regex returns undefined.
|
|
437
|
+
// Normalize to '' so both non-isRegex and isRegex paths behave the same.
|
|
438
|
+
const rawValue = hasCapture ? matchResult[segInfo.index + 1] : '';
|
|
439
|
+
const value = rawValue === undefined ? '' : rawValue;
|
|
440
|
+
|
|
342
441
|
if (usedNames.has(segInfo.name)) {
|
|
343
442
|
if (!Array.isArray(result[segInfo.name])) {
|
|
344
443
|
result[segInfo.name] = [result[segInfo.name]];
|
|
@@ -350,8 +449,19 @@ const match = (compiled, str) => {
|
|
|
350
449
|
}
|
|
351
450
|
}
|
|
352
451
|
|
|
452
|
+
// Delete empty-string entries, but preserve wildcards — an empty capture is
|
|
453
|
+
// a valid match result for wildcards. Also clean null/undefined from arrays.
|
|
353
454
|
for (const key in result) {
|
|
354
|
-
|
|
455
|
+
const val = result[key];
|
|
456
|
+
if (Array.isArray(val)) {
|
|
457
|
+
// Filter out empty-string entries from arrays (but preserve wildcards).
|
|
458
|
+
const filtered = val.filter(v => v !== '' || wildcardNames.has(key));
|
|
459
|
+
if (filtered.length === 0) {
|
|
460
|
+
delete result[key];
|
|
461
|
+
} else {
|
|
462
|
+
result[key] = filtered;
|
|
463
|
+
}
|
|
464
|
+
} else if (val === '' && !wildcardNames.has(key)) {
|
|
355
465
|
delete result[key];
|
|
356
466
|
}
|
|
357
467
|
}
|
|
@@ -380,37 +490,38 @@ const stringify = (compiled, values = {}) => {
|
|
|
380
490
|
if (segment.optional) {
|
|
381
491
|
let optionalPart = '';
|
|
382
492
|
let j = i;
|
|
383
|
-
|
|
384
|
-
|
|
493
|
+
const currentGroupId = segment.optionalGroupId;
|
|
494
|
+
|
|
495
|
+
while (j < compiled.segments.length && compiled.segments[j].optional && compiled.segments[j].optionalGroupId === currentGroupId) {
|
|
385
496
|
const seg = compiled.segments[j];
|
|
386
|
-
|
|
497
|
+
|
|
387
498
|
if (seg.type === 'literal') {
|
|
388
499
|
optionalPart += seg.name;
|
|
389
500
|
} else if (seg.type === 'named') {
|
|
390
501
|
const val = values[seg.name];
|
|
391
|
-
if (val
|
|
392
|
-
optionalPart += Array.isArray(val) ? val.join('/') : val;
|
|
393
|
-
} else {
|
|
502
|
+
if (isAbsentValue(val)) {
|
|
394
503
|
optionalPart = '';
|
|
395
504
|
break;
|
|
396
505
|
}
|
|
506
|
+
optionalPart += Array.isArray(val) ? val.join('/') : val;
|
|
397
507
|
} else if (seg.type === 'wildcard') {
|
|
398
508
|
const val = values._;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
509
|
+
// Original behaviour: wildcards are skipped in optional groups (values._
|
|
510
|
+
// is never used here). If wildcard is absent, wipe the group.
|
|
511
|
+
if (isAbsentValue(val)) {
|
|
402
512
|
optionalPart = '';
|
|
403
513
|
break;
|
|
404
514
|
}
|
|
515
|
+
optionalPart += Array.isArray(val) ? val.join('/') : val;
|
|
405
516
|
}
|
|
406
|
-
|
|
517
|
+
|
|
407
518
|
j++;
|
|
408
519
|
}
|
|
409
|
-
|
|
520
|
+
|
|
410
521
|
if (optionalPart !== '') {
|
|
411
522
|
result += optionalPart;
|
|
412
523
|
}
|
|
413
|
-
|
|
524
|
+
|
|
414
525
|
if (i === j) {
|
|
415
526
|
i++;
|
|
416
527
|
} else {
|
|
@@ -423,13 +534,15 @@ const stringify = (compiled, values = {}) => {
|
|
|
423
534
|
result += segment.name;
|
|
424
535
|
} else if (segment.type === 'named') {
|
|
425
536
|
const value = values[segment.name];
|
|
426
|
-
if (value
|
|
537
|
+
if (isAbsentValue(value)) {
|
|
427
538
|
throw new Error(`Missing required value for segment: ${segment.name}`);
|
|
428
539
|
}
|
|
429
540
|
result += Array.isArray(value) ? value.join('/') : value;
|
|
430
541
|
} else if (segment.type === 'wildcard') {
|
|
431
542
|
const value = values._;
|
|
432
|
-
|
|
543
|
+
// Empty string is a valid wildcard match result (e.g. '/files/*' on '/files/')
|
|
544
|
+
// and should be allowed without throwing.
|
|
545
|
+
if (value === undefined || value === null) {
|
|
433
546
|
throw new Error('Missing required wildcard value');
|
|
434
547
|
}
|
|
435
548
|
result += Array.isArray(value) ? value.join('/') : value;
|
|
@@ -489,4 +602,4 @@ const urlPattern = (pattern, options = {}) => {
|
|
|
489
602
|
return new UrlPattern(pattern, options);
|
|
490
603
|
};
|
|
491
604
|
|
|
492
|
-
export { DEFAULT_OPTIONS, UrlPattern,
|
|
605
|
+
export { DEFAULT_OPTIONS, UrlPattern, urlPattern as default, makePattern, makePatternFromRegex, match, stringify, urlPattern };
|
package/dist/url-pattern.umd.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(e,
|
|
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 m=e[i];if(m===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}if(m===t.optionalSegmentStartChar){s=!0,l++,p++,i++;continue}if(m===t.optionalSegmentEndChar){if(0===p)throw new Error(`Invalid pattern: unmatched '${m}' at position ${i}`);s=!1,p--,i++;continue}if(m===t.wildcardChar){o.push({type:"wildcard",name:"_",regex:".*",optional:s,optionalGroupId:s?l:void 0}),i++;continue}if(m===t.segmentNameStartChar&&i+1<e.length){const n=e.slice(i+1);let a=0;const p=t.segmentNameCharset||"",d=t.segmentNameEndChar;for(let e=0;e<n.length&&(!d||n[e]!==d)&&p.includes(n[e]);e++)a=e+1;const c=n.slice(0,a),g=!(!d||n[a]!==d);if(c.length>0){const e=t.segmentValueCharset||"",n=`([${r(e)}]+)`;o.push({type:"named",name:c,regex:n,optional:s,optionalGroupId:s?l:void 0}),i+=1+a+(g?1:0);continue}throw new Error(`Invalid pattern: '${m}' at position ${i} has no segment name`)}const d=a(e,i,t);if(d>i){const t=e.slice(i,d);o.push({type:"literal",name:t,regex:n(t),optional:s,optionalGroupId:s?l:void 0}),i=d;continue}throw new Error(`Invalid pattern: '${m}' 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=>{let t="^",n=0;const r=[];let a=0;for(;a<e.length;){const o=e[a];if(o.optional){let i="",s=a;const l=o.optionalGroupId;for(;s<e.length&&e[s].optional&&e[s].optionalGroupId===l;){const t=e[s];"wildcard"===t.type?(i+="(.*)",r.push({name:"_",index:n,type:"wildcard"}),n++):"named"===t.type?(i+=t.regex,r.push({name:t.name,index:n,type:"named"}),n++):i+=t.regex,s++}t+=`(?:${i})?`,a=s;continue}"wildcard"===o.type?(t+="(.*)",r.push({name:"_",index:n,type:"wildcard"}),n++):"named"===o.type?(t+=o.regex,r.push({name:o.name,index:n,type:"named"}),n++):t+=o.regex,a++}return t+="$",{regex:t,segmentNames:r}})(s);return{regex:l,regexObj:new RegExp(l),segments:s,segmentNames:p,options:i,isRegex:!1,pattern:e}},s=(e,n=[])=>({regex:e.source,regexObj:e,segments:[],segmentNames:n.map((e,t)=>({name:e,index:t,type:"named"})),options:t,isRegex:!0,keys:n}),l=(e,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 e=t._;if(o(e)){i="";break}i+=Array.isArray(e)?e.join("/"):e}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 e=t._;if(null==e)throw new Error("Missing required wildcard value");n+=Array.isArray(e)?e.join("/"):e}r++}return n};class m{constructor(e,t={}){if(e instanceof RegExp){const n=Array.isArray(t)?t:[];this.compiled=s(e,n)}else this.compiled=i(e,t)}match(e){return l(this.compiled,e)}stringify(e){return p(this.compiled,e)}}const d=(e,t={})=>new m(e,t);e.DEFAULT_OPTIONS=t,e.UrlPattern=m,e.default=d,e.makePattern=i,e.makePatternFromRegex=s,e.match=l,e.stringify=p,e.urlPattern=d,Object.defineProperty(e,"__esModule",{value:!0})});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peter.naydenov/url-pattern",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Matching patterns for urls and other strings. Turn strings into data or data into strings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/main.js",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@rollup/plugin-terser": "^1.0.0",
|
|
49
|
-
"@vitest/coverage-v8": "^4.1.
|
|
50
|
-
"oxlint": "^1.
|
|
51
|
-
"rollup": "^4.
|
|
52
|
-
"typescript": "^
|
|
53
|
-
"vitest": "^4.1.
|
|
49
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
50
|
+
"oxlint": "^1.73.0",
|
|
51
|
+
"rollup": "^4.62.2",
|
|
52
|
+
"typescript": "^7.0.2",
|
|
53
|
+
"vitest": "^4.1.10"
|
|
54
54
|
},
|
|
55
55
|
"allowScripts": {
|
|
56
56
|
"fsevents@2.3.3": true
|
package/{README.md → readme.md}
RENAMED
|
@@ -59,7 +59,7 @@ const pattern = urlPattern ( '/files/*' )
|
|
|
59
59
|
const result = pattern.match ( '/files/images/photo.jpg' )
|
|
60
60
|
|
|
61
61
|
console.log ( result )
|
|
62
|
-
// Output: {
|
|
62
|
+
// Output: { _: 'images/photo.jpg' }
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
### Generate URL from data
|
|
@@ -108,9 +108,9 @@ Creates a new pattern instance.
|
|
|
108
108
|
|
|
109
109
|
- **escapeChar** `{string}` - Character used for escaping special characters (default: `'\\'`)
|
|
110
110
|
- **segmentNameStartChar** `{string}` - Character that starts a named segment (default: `':'`)
|
|
111
|
-
- **segmentNameEndChar** `{string}` - Character that ends a named segment (default: `undefined`)
|
|
112
|
-
- **segmentNameCharset** `{string}` - Characters allowed in segment names (default: `'
|
|
113
|
-
- **segmentValueCharset** `{string}` - Characters allowed in segment values (default: `'
|
|
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
|
+
- **segmentNameCharset** `{string}` - Characters allowed in segment names (default: `'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'`). Treated as a list of explicit characters; range notation is not interpreted.
|
|
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
116
|
- **wildcardChar** `{string}` - Character that denotes a wildcard (default: `'*'`)
|
|
@@ -125,9 +125,9 @@ Matches a string against the pattern and returns an object with captured values,
|
|
|
125
125
|
|
|
126
126
|
Generates a URL string from provided data object.
|
|
127
127
|
|
|
128
|
-
#### `pattern.
|
|
128
|
+
#### `pattern.compiled`
|
|
129
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
131
|
|
|
132
132
|
|
|
133
133
|
|