@peter.naydenov/url-pattern 1.0.0 → 1.0.1
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/LICENSE +21 -0
- package/dist/index.js +299 -350
- package/dist/main.js +442 -0
- package/dist/url-pattern.cjs.js +503 -0
- package/dist/url-pattern.es.js +492 -0
- package/dist/url-pattern.umd.js +509 -0
- package/dist/url-pattern.umd.min.js +1 -0
- package/package.json +27 -6
- package/readme.md +3 -3
- package/src/main.js +493 -0
- package/types/index.d.ts +200 -0
- package/types/main.d.ts +200 -0
package/dist/index.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* @fileoverview URL pattern matching library
|
|
3
3
|
* @module url-pattern
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
5
|
/**
|
|
7
6
|
* @typedef {Object} UrlPatternOptions
|
|
8
7
|
* @property {string} [escapeChar='\\'] - Character used for escaping special characters
|
|
@@ -13,7 +12,6 @@
|
|
|
13
12
|
* @property {string} [optionalSegmentEndChar=')'] - Character that ends an optional segment
|
|
14
13
|
* @property {string} [wildcardChar='*'] - Character that denotes a wildcard
|
|
15
14
|
*/
|
|
16
|
-
|
|
17
15
|
/**
|
|
18
16
|
* @typedef {Object} ParsedSegment
|
|
19
17
|
* @property {string} name - Segment name
|
|
@@ -21,14 +19,12 @@
|
|
|
21
19
|
* @property {boolean} [optional=false] - Whether the segment is optional
|
|
22
20
|
* @property {string} regex - Compiled regex string
|
|
23
21
|
*/
|
|
24
|
-
|
|
25
22
|
/**
|
|
26
23
|
* @typedef {Object} SegmentName
|
|
27
24
|
* @property {string} name - Segment name
|
|
28
25
|
* @property {number} index - Capture group index
|
|
29
26
|
* @property {string} type - Segment type ('named' | 'wildcard')
|
|
30
27
|
*/
|
|
31
|
-
|
|
32
28
|
/**
|
|
33
29
|
* @typedef {Object} CompiledPattern
|
|
34
30
|
* @property {string} regex - Compiled regex string
|
|
@@ -40,38 +36,34 @@
|
|
|
40
36
|
* @property {string} [pattern] - Original pattern string
|
|
41
37
|
* @property {Array<string>} [keys] - Keys for regex patterns
|
|
42
38
|
*/
|
|
43
|
-
|
|
44
39
|
/**
|
|
45
40
|
* Default options for URL pattern matching
|
|
46
41
|
* @type {UrlPatternOptions}
|
|
47
42
|
*/
|
|
48
43
|
const DEFAULT_OPTIONS = {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
escapeChar: '\\',
|
|
45
|
+
segmentNameStartChar: ':',
|
|
46
|
+
segmentNameCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
|
|
47
|
+
segmentValueCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~ %',
|
|
48
|
+
optionalSegmentStartChar: '(',
|
|
49
|
+
optionalSegmentEndChar: ')',
|
|
50
|
+
wildcardChar: '*'
|
|
56
51
|
};
|
|
57
|
-
|
|
58
52
|
/**
|
|
59
53
|
* Escapes special regex characters in a string
|
|
60
54
|
* @param {string} str - String to escape
|
|
61
55
|
* @returns {string} Escaped string
|
|
62
56
|
*/
|
|
63
57
|
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
64
|
-
|
|
65
58
|
/**
|
|
66
59
|
* Merges default options with user provided options
|
|
67
60
|
* @param {UrlPatternOptions} [userOptions={}] - User provided options
|
|
68
61
|
* @returns {UrlPatternOptions} Merged options
|
|
69
62
|
*/
|
|
70
63
|
const mergeOptions = (userOptions = {}) => ({
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
...DEFAULT_OPTIONS,
|
|
65
|
+
...userOptions
|
|
73
66
|
});
|
|
74
|
-
|
|
75
67
|
/**
|
|
76
68
|
* Finds the position of the next special character in the pattern
|
|
77
69
|
* @param {string} pattern - Pattern string
|
|
@@ -80,28 +72,25 @@ const mergeOptions = (userOptions = {}) => ({
|
|
|
80
72
|
* @returns {number} Position of next special character
|
|
81
73
|
*/
|
|
82
74
|
const findNextSpecialChar = (pattern, start, options) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
75
|
+
const chars = [
|
|
76
|
+
options.escapeChar,
|
|
77
|
+
options.optionalSegmentStartChar,
|
|
78
|
+
options.optionalSegmentEndChar,
|
|
79
|
+
options.wildcardChar,
|
|
80
|
+
options.segmentNameStartChar
|
|
81
|
+
];
|
|
82
|
+
let minPos = pattern.length;
|
|
83
|
+
for (let i = 0; i < chars.length; i++) {
|
|
84
|
+
const char = chars[i];
|
|
85
|
+
if (!char)
|
|
86
|
+
continue;
|
|
87
|
+
const pos = pattern.indexOf(char, start);
|
|
88
|
+
if (pos !== -1 && pos < minPos) {
|
|
89
|
+
minPos = pos;
|
|
90
|
+
}
|
|
99
91
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return minPos;
|
|
92
|
+
return minPos;
|
|
103
93
|
};
|
|
104
|
-
|
|
105
94
|
/**
|
|
106
95
|
* Parses a pattern string into segments
|
|
107
96
|
* @param {string} pattern - Pattern string to parse
|
|
@@ -109,101 +98,86 @@ const findNextSpecialChar = (pattern, start, options) => {
|
|
|
109
98
|
* @returns {Array<ParsedSegment>} Parsed segments
|
|
110
99
|
*/
|
|
111
100
|
const parsePattern = (pattern, options) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
i += 2;
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (char === options.optionalSegmentStartChar) {
|
|
131
|
-
inOptional = true;
|
|
132
|
-
i++;
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (char === options.optionalSegmentEndChar) {
|
|
137
|
-
inOptional = false;
|
|
138
|
-
i++;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (char === options.wildcardChar) {
|
|
143
|
-
segments.push({
|
|
144
|
-
type: 'wildcard',
|
|
145
|
-
name: '_',
|
|
146
|
-
regex: '.*',
|
|
147
|
-
optional: inOptional
|
|
148
|
-
});
|
|
149
|
-
i++;
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (char === options.segmentNameStartChar && i + 1 < pattern.length) {
|
|
154
|
-
const remaining = pattern.slice(i + 1);
|
|
155
|
-
let nameEnd = 0;
|
|
156
|
-
const charset = options.segmentNameCharset || '';
|
|
157
|
-
|
|
158
|
-
for (let j = 0; j < remaining.length; j++) {
|
|
159
|
-
if (!charset.includes(remaining[j])) {
|
|
160
|
-
break;
|
|
101
|
+
const segments = [];
|
|
102
|
+
let i = 0;
|
|
103
|
+
let inOptional = false;
|
|
104
|
+
while (i < pattern.length) {
|
|
105
|
+
const char = pattern[i];
|
|
106
|
+
if (char === options.escapeChar && i + 1 < pattern.length) {
|
|
107
|
+
segments.push({
|
|
108
|
+
type: 'literal',
|
|
109
|
+
name: pattern[i + 1],
|
|
110
|
+
regex: escapeRegex(pattern[i + 1]),
|
|
111
|
+
optional: inOptional
|
|
112
|
+
});
|
|
113
|
+
i += 2;
|
|
114
|
+
continue;
|
|
161
115
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
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 += '-';
|
|
116
|
+
if (char === options.optionalSegmentStartChar) {
|
|
117
|
+
inOptional = true;
|
|
118
|
+
i++;
|
|
119
|
+
continue;
|
|
172
120
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
121
|
+
if (char === options.optionalSegmentEndChar) {
|
|
122
|
+
inOptional = false;
|
|
123
|
+
i++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (char === options.wildcardChar) {
|
|
127
|
+
segments.push({
|
|
128
|
+
type: 'wildcard',
|
|
129
|
+
name: '_',
|
|
130
|
+
regex: '.*',
|
|
131
|
+
optional: inOptional
|
|
132
|
+
});
|
|
133
|
+
i++;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (char === options.segmentNameStartChar && i + 1 < pattern.length) {
|
|
137
|
+
const remaining = pattern.slice(i + 1);
|
|
138
|
+
let nameEnd = 0;
|
|
139
|
+
const charset = options.segmentNameCharset || '';
|
|
140
|
+
for (let j = 0; j < remaining.length; j++) {
|
|
141
|
+
if (!charset.includes(remaining[j])) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
nameEnd = j + 1;
|
|
145
|
+
}
|
|
146
|
+
const name = remaining.slice(0, nameEnd);
|
|
147
|
+
if (name.length > 0) {
|
|
148
|
+
let valueCharset = options.segmentValueCharset || '';
|
|
149
|
+
if (valueCharset.includes('-') && valueCharset.indexOf('-') > 0 && valueCharset.indexOf('-') < valueCharset.length - 1) {
|
|
150
|
+
valueCharset = valueCharset.replace(/-/g, '');
|
|
151
|
+
valueCharset += '-';
|
|
152
|
+
}
|
|
153
|
+
const escapedValueCharset = valueCharset.replace(/\]/g, '\\]');
|
|
154
|
+
const valueRegex = `([${escapedValueCharset}]+)`;
|
|
155
|
+
segments.push({
|
|
156
|
+
type: 'named',
|
|
157
|
+
name,
|
|
158
|
+
regex: valueRegex,
|
|
159
|
+
optional: inOptional
|
|
160
|
+
});
|
|
161
|
+
i += 1 + nameEnd;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const literalEnd = findNextSpecialChar(pattern, i, options);
|
|
166
|
+
if (literalEnd > i) {
|
|
167
|
+
const literal = pattern.slice(i, literalEnd);
|
|
168
|
+
segments.push({
|
|
169
|
+
type: 'literal',
|
|
170
|
+
name: literal,
|
|
171
|
+
regex: escapeRegex(literal),
|
|
172
|
+
optional: inOptional
|
|
173
|
+
});
|
|
174
|
+
i = literalEnd;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
i++;
|
|
199
178
|
}
|
|
200
|
-
|
|
201
|
-
i++;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return segments;
|
|
179
|
+
return segments;
|
|
205
180
|
};
|
|
206
|
-
|
|
207
181
|
/**
|
|
208
182
|
* Compiles segments into a regex pattern
|
|
209
183
|
* @param {Array<ParsedSegment>} segments - Parsed segments
|
|
@@ -211,62 +185,55 @@ const parsePattern = (pattern, options) => {
|
|
|
211
185
|
* @returns {{regex: string, segmentNames: Array<SegmentName>}} Compiled regex and segment names
|
|
212
186
|
*/
|
|
213
187
|
const compileRegex = (segments, options) => {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
188
|
+
let regex = '^';
|
|
189
|
+
let groupIndex = 0;
|
|
190
|
+
/** @type {Array<SegmentName>} */
|
|
191
|
+
const segmentNames = [];
|
|
192
|
+
let i = 0;
|
|
193
|
+
while (i < segments.length) {
|
|
194
|
+
const segment = segments[i];
|
|
195
|
+
if (segment.optional) {
|
|
196
|
+
let optionalPart = '';
|
|
197
|
+
let j = i;
|
|
198
|
+
while (j < segments.length && segments[j].optional) {
|
|
199
|
+
const seg = segments[j];
|
|
200
|
+
if (seg.type === 'wildcard') {
|
|
201
|
+
optionalPart += '(.*)';
|
|
202
|
+
segmentNames.push({ name: '_', index: groupIndex, type: 'wildcard' });
|
|
203
|
+
groupIndex++;
|
|
204
|
+
}
|
|
205
|
+
else if (seg.type === 'named') {
|
|
206
|
+
optionalPart += seg.regex;
|
|
207
|
+
segmentNames.push({ name: seg.name, index: groupIndex, type: 'named' });
|
|
208
|
+
groupIndex++;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
optionalPart += seg.regex;
|
|
212
|
+
}
|
|
213
|
+
j++;
|
|
214
|
+
}
|
|
215
|
+
regex += `(?:${optionalPart})?`;
|
|
216
|
+
i = j;
|
|
217
|
+
continue;
|
|
240
218
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
regex += segment.regex;
|
|
256
|
-
segmentNames.push({ name: segment.name, index: groupIndex, type: 'named' });
|
|
257
|
-
groupIndex++;
|
|
258
|
-
} else {
|
|
259
|
-
regex += segment.regex;
|
|
219
|
+
if (segment.type === 'wildcard') {
|
|
220
|
+
regex += '(.*)';
|
|
221
|
+
segmentNames.push({ name: '_', index: groupIndex, type: 'wildcard' });
|
|
222
|
+
groupIndex++;
|
|
223
|
+
}
|
|
224
|
+
else if (segment.type === 'named') {
|
|
225
|
+
regex += segment.regex;
|
|
226
|
+
segmentNames.push({ name: segment.name, index: groupIndex, type: 'named' });
|
|
227
|
+
groupIndex++;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
regex += segment.regex;
|
|
231
|
+
}
|
|
232
|
+
i++;
|
|
260
233
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
regex += '$';
|
|
266
|
-
|
|
267
|
-
return { regex, segmentNames };
|
|
234
|
+
regex += '$';
|
|
235
|
+
return { regex, segmentNames };
|
|
268
236
|
};
|
|
269
|
-
|
|
270
237
|
/**
|
|
271
238
|
* Creates a compiled pattern from a string
|
|
272
239
|
* @param {string} pattern - Pattern string
|
|
@@ -274,21 +241,19 @@ const compileRegex = (segments, options) => {
|
|
|
274
241
|
* @returns {CompiledPattern} Compiled pattern
|
|
275
242
|
*/
|
|
276
243
|
const makePattern = (pattern, options = {}) => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
};
|
|
244
|
+
const mergedOptions = mergeOptions(options);
|
|
245
|
+
const segments = parsePattern(pattern, mergedOptions);
|
|
246
|
+
const { regex, segmentNames } = compileRegex(segments, mergedOptions);
|
|
247
|
+
return {
|
|
248
|
+
regex,
|
|
249
|
+
regexObj: new RegExp(regex),
|
|
250
|
+
segments,
|
|
251
|
+
segmentNames,
|
|
252
|
+
options: mergedOptions,
|
|
253
|
+
isRegex: false,
|
|
254
|
+
pattern
|
|
255
|
+
};
|
|
290
256
|
};
|
|
291
|
-
|
|
292
257
|
/**
|
|
293
258
|
* Creates a compiled pattern from a regex
|
|
294
259
|
* @param {RegExp} regex - Regex pattern
|
|
@@ -296,17 +261,16 @@ const makePattern = (pattern, options = {}) => {
|
|
|
296
261
|
* @returns {CompiledPattern} Compiled pattern
|
|
297
262
|
*/
|
|
298
263
|
const makePatternFromRegex = (regex, keys = []) => {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
264
|
+
return {
|
|
265
|
+
regex: regex.source,
|
|
266
|
+
regexObj: regex,
|
|
267
|
+
segments: [],
|
|
268
|
+
segmentNames: keys.map((name, index) => ({ name, index, type: 'named' })),
|
|
269
|
+
options: DEFAULT_OPTIONS,
|
|
270
|
+
isRegex: true,
|
|
271
|
+
keys
|
|
272
|
+
};
|
|
308
273
|
};
|
|
309
|
-
|
|
310
274
|
/**
|
|
311
275
|
* Matches a string against a compiled pattern
|
|
312
276
|
* @param {CompiledPattern} compiled - Compiled pattern
|
|
@@ -314,51 +278,44 @@ const makePatternFromRegex = (regex, keys = []) => {
|
|
|
314
278
|
* @returns {Object|null} Extracted values or null if no match
|
|
315
279
|
*/
|
|
316
280
|
const match = (compiled, str) => {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
281
|
+
const matchResult = compiled.regexObj.exec(str);
|
|
282
|
+
if (!matchResult) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
if (compiled.isRegex) {
|
|
286
|
+
if (compiled.keys && compiled.keys.length > 0) {
|
|
287
|
+
const result = {};
|
|
288
|
+
compiled.keys.forEach((key, index) => {
|
|
289
|
+
const val = matchResult[index + 1];
|
|
290
|
+
result[key] = val !== undefined ? val : null;
|
|
291
|
+
});
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
return matchResult.slice(1);
|
|
331
295
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
} else {
|
|
348
|
-
usedNames.add(segInfo.name);
|
|
349
|
-
result[segInfo.name] = value;
|
|
296
|
+
const result = {};
|
|
297
|
+
const usedNames = new Set();
|
|
298
|
+
for (let i = 0; i < compiled.segmentNames.length; i++) {
|
|
299
|
+
const segInfo = compiled.segmentNames[i];
|
|
300
|
+
const value = matchResult[segInfo.index + 1] || '';
|
|
301
|
+
if (usedNames.has(segInfo.name)) {
|
|
302
|
+
if (!Array.isArray(result[segInfo.name])) {
|
|
303
|
+
result[segInfo.name] = [result[segInfo.name]];
|
|
304
|
+
}
|
|
305
|
+
result[segInfo.name].push(value);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
usedNames.add(segInfo.name);
|
|
309
|
+
result[segInfo.name] = value;
|
|
310
|
+
}
|
|
350
311
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
delete result[key];
|
|
312
|
+
for (const key in result) {
|
|
313
|
+
if (result[key] === '') {
|
|
314
|
+
delete result[key];
|
|
315
|
+
}
|
|
356
316
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return result;
|
|
317
|
+
return result;
|
|
360
318
|
};
|
|
361
|
-
|
|
362
319
|
/**
|
|
363
320
|
* Stringifies a pattern with given values
|
|
364
321
|
* @param {CompiledPattern} compiled - Compiled pattern
|
|
@@ -367,118 +324,111 @@ const match = (compiled, str) => {
|
|
|
367
324
|
* @throws {Error} If required values are missing
|
|
368
325
|
*/
|
|
369
326
|
const stringify = (compiled, values = {}) => {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
327
|
+
if (compiled.isRegex) {
|
|
328
|
+
throw new Error('Cannot stringify a pattern created from regex');
|
|
329
|
+
}
|
|
330
|
+
let result = '';
|
|
331
|
+
let i = 0;
|
|
332
|
+
while (i < compiled.segments.length) {
|
|
333
|
+
const segment = compiled.segments[i];
|
|
334
|
+
if (segment.optional) {
|
|
335
|
+
let optionalPart = '';
|
|
336
|
+
let j = i;
|
|
337
|
+
while (j < compiled.segments.length && compiled.segments[j].optional) {
|
|
338
|
+
const seg = compiled.segments[j];
|
|
339
|
+
if (seg.type === 'literal') {
|
|
340
|
+
optionalPart += seg.name;
|
|
341
|
+
}
|
|
342
|
+
else if (seg.type === 'named') {
|
|
343
|
+
const val = values[seg.name];
|
|
344
|
+
if (val !== undefined && val !== null && val !== '') {
|
|
345
|
+
optionalPart += Array.isArray(val) ? val.join('/') : val;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
optionalPart = '';
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (seg.type === 'wildcard') {
|
|
353
|
+
const val = values._;
|
|
354
|
+
if (val !== undefined && val !== null && val !== '') {
|
|
355
|
+
optionalPart += Array.isArray(val) ? val.join('/') : val;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
optionalPart = '';
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
j++;
|
|
363
|
+
}
|
|
364
|
+
if (optionalPart !== '') {
|
|
365
|
+
result += optionalPart;
|
|
366
|
+
}
|
|
367
|
+
if (i === j) {
|
|
368
|
+
i++;
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
i = j;
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (segment.type === 'literal') {
|
|
376
|
+
result += segment.name;
|
|
377
|
+
}
|
|
378
|
+
else if (segment.type === 'named') {
|
|
379
|
+
const value = values[segment.name];
|
|
380
|
+
if (value === undefined || value === null || value === '') {
|
|
381
|
+
throw new Error(`Missing required value for segment: ${segment.name}`);
|
|
382
|
+
}
|
|
383
|
+
result += Array.isArray(value) ? value.join('/') : value;
|
|
384
|
+
}
|
|
385
|
+
else if (segment.type === 'wildcard') {
|
|
386
|
+
const value = values._;
|
|
387
|
+
if (value === undefined || value === null || value === '') {
|
|
388
|
+
throw new Error('Missing required wildcard value');
|
|
389
|
+
}
|
|
390
|
+
result += Array.isArray(value) ? value.join('/') : value;
|
|
405
391
|
}
|
|
406
|
-
|
|
407
|
-
j++;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (optionalPart !== '') {
|
|
411
|
-
result += optionalPart;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (i === j) {
|
|
415
392
|
i++;
|
|
416
|
-
} else {
|
|
417
|
-
i = j;
|
|
418
|
-
}
|
|
419
|
-
continue;
|
|
420
393
|
}
|
|
421
|
-
|
|
422
|
-
if (segment.type === 'literal') {
|
|
423
|
-
result += segment.name;
|
|
424
|
-
} else if (segment.type === 'named') {
|
|
425
|
-
const value = values[segment.name];
|
|
426
|
-
if (value === undefined || value === null || value === '') {
|
|
427
|
-
throw new Error(`Missing required value for segment: ${segment.name}`);
|
|
428
|
-
}
|
|
429
|
-
result += Array.isArray(value) ? value.join('/') : value;
|
|
430
|
-
} else if (segment.type === 'wildcard') {
|
|
431
|
-
const value = values._;
|
|
432
|
-
if (value === undefined || value === null || value === '') {
|
|
433
|
-
throw new Error('Missing required wildcard value');
|
|
434
|
-
}
|
|
435
|
-
result += Array.isArray(value) ? value.join('/') : value;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
i++;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return result;
|
|
394
|
+
return result;
|
|
442
395
|
};
|
|
443
|
-
|
|
444
396
|
/**
|
|
445
397
|
* UrlPattern class for matching and generating URLs
|
|
446
398
|
*/
|
|
447
399
|
class UrlPattern {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
400
|
+
/**
|
|
401
|
+
* @param {string|RegExp} pattern - Pattern string or regex
|
|
402
|
+
* @param {UrlPatternOptions|Array<string>} [options={}] - Options or keys (for regex)
|
|
403
|
+
*/
|
|
404
|
+
constructor(pattern, options = {}) {
|
|
405
|
+
if (pattern instanceof RegExp) {
|
|
406
|
+
const keys = Array.isArray(options) ? options : [];
|
|
407
|
+
/** @type {CompiledPattern} */
|
|
408
|
+
this.compiled = makePatternFromRegex(pattern, keys);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
/** @type {CompiledPattern} */
|
|
412
|
+
this.compiled = makePattern(pattern, /** @type {UrlPatternOptions} */ (options));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Match a string against the pattern
|
|
417
|
+
* @param {string} str - String to match
|
|
418
|
+
* @returns {Object|null} Extracted values or null if no match
|
|
419
|
+
*/
|
|
420
|
+
match(str) {
|
|
421
|
+
return match(this.compiled, str);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Generate a string from the pattern
|
|
425
|
+
* @param {Object} [values={}] - Values to stringify
|
|
426
|
+
* @returns {string} Generated string
|
|
427
|
+
*/
|
|
428
|
+
stringify(values) {
|
|
429
|
+
return stringify(this.compiled, values);
|
|
460
430
|
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Match a string against the pattern
|
|
465
|
-
* @param {string} str - String to match
|
|
466
|
-
* @returns {Object|null} Extracted values or null if no match
|
|
467
|
-
*/
|
|
468
|
-
match(str) {
|
|
469
|
-
return match(this.compiled, str);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Generate a string from the pattern
|
|
474
|
-
* @param {Object} [values={}] - Values to stringify
|
|
475
|
-
* @returns {string} Generated string
|
|
476
|
-
*/
|
|
477
|
-
stringify(values) {
|
|
478
|
-
return stringify(this.compiled, values);
|
|
479
|
-
}
|
|
480
431
|
}
|
|
481
|
-
|
|
482
432
|
/**
|
|
483
433
|
* Creates a new UrlPattern instance (functional API)
|
|
484
434
|
* @param {string|RegExp} pattern - Pattern string or regex
|
|
@@ -486,8 +436,7 @@ class UrlPattern {
|
|
|
486
436
|
* @returns {UrlPattern} UrlPattern instance
|
|
487
437
|
*/
|
|
488
438
|
const urlPattern = (pattern, options = {}) => {
|
|
489
|
-
|
|
439
|
+
return new UrlPattern(pattern, options);
|
|
490
440
|
};
|
|
491
|
-
|
|
492
441
|
export { UrlPattern, urlPattern, makePattern, makePatternFromRegex, match, stringify, DEFAULT_OPTIONS };
|
|
493
442
|
export default UrlPattern;
|