@peter.naydenov/url-pattern 1.0.0 → 1.0.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Peter Naydenov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,9 +1,9 @@
1
1
  # URL Pattern (@peter.naydenov/url-pattern)
2
2
 
3
3
  [![version](https://img.shields.io/npm/v/@peter.naydenov/url-pattern.svg)](https://www.npmjs.com/package/@peter.naydenov/url-pattern)
4
- [![license](https://img.shields.io/github/license/PeterNaydenov/git-url-pattern.svg)](https://github.com/PeterNaydenov/git-url-pattern/blob/main/LICENSE)
5
- [![GitHub issues](https://img.shields.io/github/issues-raw/PeterNaydenov/git-url-pattern.svg)](https://github.com/PeterNaydenov/git-url-pattern/issues)
6
- [![GitHub top language](https://img.shields.io/github/languages/top/PeterNaydenov/git-url-pattern.svg)](https://github.com/PeterNaydenov/git-url-pattern)
4
+ [![license](https://img.shields.io/github/license/PeterNaydenov/url-pattern.svg)](https://github.com/PeterNaydenov/url-pattern/blob/main/LICENSE)
5
+ [![GitHub issues](https://img.shields.io/github/issues-raw/PeterNaydenov/url-pattern.svg)](https://github.com/PeterNaydenov/url-pattern/issues)
6
+ [![GitHub top language](https://img.shields.io/github/languages/top/PeterNaydenov/url-pattern.svg)](https://github.com/PeterNaydenov/url-pattern)
7
7
  [![npm bundle size](https://img.shields.io/bundlejs/size/@peter.naydenov/url-pattern.svg)](https://www.npmjs.com/package/@peter.naydenov/url-pattern)
8
8
 
9
9
  Easier than regex string matching patterns for urls and other strings. Turn strings into data or data into strings.
@@ -89,6 +89,11 @@ const result = pattern.match ( '/user/john/post/123' )
89
89
  console.log ( result )
90
90
  // Output: { username: 'john', postId: '123' }
91
91
  ```
92
+ ## URL Patterns
93
+
94
+ See [supported URL pattern types with examples](./PATTERNS.md).
95
+
96
+
92
97
 
93
98
  ## API Reference
94
99
 
@@ -124,13 +129,21 @@ Generates a URL string from provided data object.
124
129
 
125
130
  Returns an object with `regex` (compiled RegExp), `segments` (parsed segments array), and `segmentNames` (segment name mappings).
126
131
 
127
- ## Links
128
132
 
133
+
134
+ ## Links
135
+ - [Supported URL patterns with examples](./PATTERNS.md).
129
136
  - [History of changes](CHANGELOG.md)
130
137
  - [TypeScript definitions](types/index.d.ts)
131
138
 
139
+
140
+
132
141
  ## Credits
133
142
 
134
143
  '@peter.naydenov/url-pattern' was created and supported by [Peter Naydenov](https://github.com/PeterNaydenov).
135
144
 
145
+
146
+
147
+ ## License
148
+
136
149
  '@peter.naydenov/url-pattern' is released under the [MIT License](https://github.com/PeterNaydenov/git-url-pattern/blob/main/LICENSE).
@@ -0,0 +1,503 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ /**
6
+ * @fileoverview URL pattern matching library
7
+ * @module url-pattern
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} UrlPatternOptions
12
+ * @property {string} [escapeChar='\\'] - Character used for escaping special characters
13
+ * @property {string} [segmentNameStartChar=':'] - Character that starts a named segment
14
+ * @property {string} [segmentNameCharset='a-zA-Z0-9'] - Characters allowed in segment names
15
+ * @property {string} [segmentValueCharset='a-zA-Z0-9-_~ %'] - Characters allowed in segment values
16
+ * @property {string} [optionalSegmentStartChar='('] - Character that starts an optional segment
17
+ * @property {string} [optionalSegmentEndChar=')'] - Character that ends an optional segment
18
+ * @property {string} [wildcardChar='*'] - Character that denotes a wildcard
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} ParsedSegment
23
+ * @property {string} name - Segment name
24
+ * @property {string} type - Segment type ('named' | 'wildcard' | 'literal')
25
+ * @property {boolean} [optional=false] - Whether the segment is optional
26
+ * @property {string} regex - Compiled regex string
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} SegmentName
31
+ * @property {string} name - Segment name
32
+ * @property {number} index - Capture group index
33
+ * @property {string} type - Segment type ('named' | 'wildcard')
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} CompiledPattern
38
+ * @property {string} regex - Compiled regex string
39
+ * @property {RegExp} regexObj - Compiled regex object
40
+ * @property {Array<ParsedSegment>} segments - Parsed segments
41
+ * @property {Array<SegmentName>} segmentNames - Segment name mappings
42
+ * @property {UrlPatternOptions} options - Options used
43
+ * @property {boolean} isRegex - Whether pattern was created from regex
44
+ * @property {string} [pattern] - Original pattern string
45
+ * @property {Array<string>} [keys] - Keys for regex patterns
46
+ */
47
+
48
+ /**
49
+ * Default options for URL pattern matching
50
+ * @type {UrlPatternOptions}
51
+ */
52
+ const DEFAULT_OPTIONS = {
53
+ escapeChar: '\\',
54
+ segmentNameStartChar: ':',
55
+ segmentNameCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
56
+ segmentValueCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~ %',
57
+ optionalSegmentStartChar: '(',
58
+ optionalSegmentEndChar: ')',
59
+ wildcardChar: '*'
60
+ };
61
+
62
+ /**
63
+ * Escapes special regex characters in a string
64
+ * @param {string} str - String to escape
65
+ * @returns {string} Escaped string
66
+ */
67
+ const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
68
+
69
+ /**
70
+ * Merges default options with user provided options
71
+ * @param {UrlPatternOptions} [userOptions={}] - User provided options
72
+ * @returns {UrlPatternOptions} Merged options
73
+ */
74
+ const mergeOptions = (userOptions = {}) => ({
75
+ ...DEFAULT_OPTIONS,
76
+ ...userOptions
77
+ });
78
+
79
+ /**
80
+ * Finds the position of the next special character in the pattern
81
+ * @param {string} pattern - Pattern string
82
+ * @param {number} start - Starting position
83
+ * @param {UrlPatternOptions} options - Parsing options
84
+ * @returns {number} Position of next special character
85
+ */
86
+ const findNextSpecialChar = (pattern, start, options) => {
87
+ const chars = [
88
+ options.escapeChar,
89
+ options.optionalSegmentStartChar,
90
+ options.optionalSegmentEndChar,
91
+ options.wildcardChar,
92
+ options.segmentNameStartChar
93
+ ];
94
+
95
+ let minPos = pattern.length;
96
+
97
+ for (let i = 0; i < chars.length; i++) {
98
+ const char = chars[i];
99
+ if (!char) continue;
100
+ const pos = pattern.indexOf(char, start);
101
+ if (pos !== -1 && pos < minPos) {
102
+ minPos = pos;
103
+ }
104
+ }
105
+
106
+ return minPos;
107
+ };
108
+
109
+ /**
110
+ * Parses a pattern string into segments
111
+ * @param {string} pattern - Pattern string to parse
112
+ * @param {UrlPatternOptions} options - Parsing options
113
+ * @returns {Array<ParsedSegment>} Parsed segments
114
+ */
115
+ const parsePattern = (pattern, options) => {
116
+ const segments = [];
117
+ let i = 0;
118
+ let inOptional = false;
119
+
120
+ while (i < pattern.length) {
121
+ const char = pattern[i];
122
+
123
+ if (char === options.escapeChar && i + 1 < pattern.length) {
124
+ segments.push({
125
+ type: 'literal',
126
+ name: pattern[i + 1],
127
+ regex: escapeRegex(pattern[i + 1]),
128
+ optional: inOptional
129
+ });
130
+ i += 2;
131
+ continue;
132
+ }
133
+
134
+ if (char === options.optionalSegmentStartChar) {
135
+ inOptional = true;
136
+ i++;
137
+ continue;
138
+ }
139
+
140
+ if (char === options.optionalSegmentEndChar) {
141
+ inOptional = false;
142
+ i++;
143
+ continue;
144
+ }
145
+
146
+ if (char === options.wildcardChar) {
147
+ segments.push({
148
+ type: 'wildcard',
149
+ name: '_',
150
+ regex: '.*',
151
+ optional: inOptional
152
+ });
153
+ i++;
154
+ continue;
155
+ }
156
+
157
+ if (char === options.segmentNameStartChar && i + 1 < pattern.length) {
158
+ const remaining = pattern.slice(i + 1);
159
+ let nameEnd = 0;
160
+ const charset = options.segmentNameCharset || '';
161
+
162
+ for (let j = 0; j < remaining.length; j++) {
163
+ if (!charset.includes(remaining[j])) {
164
+ break;
165
+ }
166
+ nameEnd = j + 1;
167
+ }
168
+
169
+ const name = remaining.slice(0, nameEnd);
170
+
171
+ if (name.length > 0) {
172
+ let valueCharset = options.segmentValueCharset || '';
173
+ if (valueCharset.includes('-') && valueCharset.indexOf('-') > 0 && valueCharset.indexOf('-') < valueCharset.length - 1) {
174
+ valueCharset = valueCharset.replace(/-/g, '');
175
+ valueCharset += '-';
176
+ }
177
+ const escapedValueCharset = valueCharset.replace(/\]/g, '\\]');
178
+ const valueRegex = `([${escapedValueCharset}]+)`;
179
+
180
+ segments.push({
181
+ type: 'named',
182
+ name,
183
+ regex: valueRegex,
184
+ optional: inOptional
185
+ });
186
+ i += 1 + nameEnd;
187
+ continue;
188
+ }
189
+ }
190
+
191
+ const literalEnd = findNextSpecialChar(pattern, i, options);
192
+
193
+ if (literalEnd > i) {
194
+ const literal = pattern.slice(i, literalEnd);
195
+ segments.push({
196
+ type: 'literal',
197
+ name: literal,
198
+ regex: escapeRegex(literal),
199
+ optional: inOptional
200
+ });
201
+ i = literalEnd;
202
+ continue;
203
+ }
204
+
205
+ i++;
206
+ }
207
+
208
+ return segments;
209
+ };
210
+
211
+ /**
212
+ * Compiles segments into a regex pattern
213
+ * @param {Array<ParsedSegment>} segments - Parsed segments
214
+ * @param {UrlPatternOptions} options - Options
215
+ * @returns {{regex: string, segmentNames: Array<SegmentName>}} Compiled regex and segment names
216
+ */
217
+ const compileRegex = (segments, _options) => {
218
+ let regex = '^';
219
+ let groupIndex = 0;
220
+ /** @type {Array<SegmentName>} */
221
+ const segmentNames = [];
222
+ let i = 0;
223
+
224
+ while (i < segments.length) {
225
+ const segment = segments[i];
226
+
227
+ if (segment.optional) {
228
+ let optionalPart = '';
229
+ let j = i;
230
+
231
+ while (j < segments.length && segments[j].optional) {
232
+ const seg = segments[j];
233
+
234
+ if (seg.type === 'wildcard') {
235
+ optionalPart += '(.*)';
236
+ segmentNames.push({ name: '_', index: groupIndex, type: 'wildcard' });
237
+ groupIndex++;
238
+ } else if (seg.type === 'named') {
239
+ optionalPart += seg.regex;
240
+ segmentNames.push({ name: seg.name, index: groupIndex, type: 'named' });
241
+ groupIndex++;
242
+ } else {
243
+ optionalPart += seg.regex;
244
+ }
245
+
246
+ j++;
247
+ }
248
+
249
+ regex += `(?:${optionalPart})?`;
250
+ i = j;
251
+ continue;
252
+ }
253
+
254
+ if (segment.type === 'wildcard') {
255
+ regex += '(.*)';
256
+ segmentNames.push({ name: '_', index: groupIndex, type: 'wildcard' });
257
+ groupIndex++;
258
+ } else if (segment.type === 'named') {
259
+ regex += segment.regex;
260
+ segmentNames.push({ name: segment.name, index: groupIndex, type: 'named' });
261
+ groupIndex++;
262
+ } else {
263
+ regex += segment.regex;
264
+ }
265
+
266
+ i++;
267
+ }
268
+
269
+ regex += '$';
270
+
271
+ return { regex, segmentNames };
272
+ };
273
+
274
+ /**
275
+ * Creates a compiled pattern from a string
276
+ * @param {string} pattern - Pattern string
277
+ * @param {UrlPatternOptions} [options={}] - Options
278
+ * @returns {CompiledPattern} Compiled pattern
279
+ */
280
+ const makePattern = (pattern, options = {}) => {
281
+ const mergedOptions = mergeOptions(options);
282
+ const segments = parsePattern(pattern, mergedOptions);
283
+ const { regex, segmentNames } = compileRegex(segments);
284
+
285
+ return {
286
+ regex,
287
+ regexObj: new RegExp(regex),
288
+ segments,
289
+ segmentNames,
290
+ options: mergedOptions,
291
+ isRegex: false,
292
+ pattern
293
+ };
294
+ };
295
+
296
+ /**
297
+ * Creates a compiled pattern from a regex
298
+ * @param {RegExp} regex - Regex pattern
299
+ * @param {Array<string>} [keys=[]] - Array of key names for captured groups
300
+ * @returns {CompiledPattern} Compiled pattern
301
+ */
302
+ const makePatternFromRegex = (regex, keys = []) => {
303
+ return {
304
+ regex: regex.source,
305
+ regexObj: regex,
306
+ segments: [],
307
+ segmentNames: keys.map((name, index) => ({ name, index, type: 'named' })),
308
+ options: DEFAULT_OPTIONS,
309
+ isRegex: true,
310
+ keys
311
+ };
312
+ };
313
+
314
+ /**
315
+ * Matches a string against a compiled pattern
316
+ * @param {CompiledPattern} compiled - Compiled pattern
317
+ * @param {string} str - String to match
318
+ * @returns {Object|null} Extracted values or null if no match
319
+ */
320
+ const match = (compiled, str) => {
321
+ const matchResult = compiled.regexObj.exec(str);
322
+
323
+ if (!matchResult) {
324
+ return null;
325
+ }
326
+
327
+ if (compiled.isRegex) {
328
+ if (compiled.keys && compiled.keys.length > 0) {
329
+ const result = {};
330
+ compiled.keys.forEach((key, index) => {
331
+ const val = matchResult[index + 1];
332
+ result[key] = val !== undefined ? val : null;
333
+ });
334
+ return result;
335
+ }
336
+ return matchResult.slice(1);
337
+ }
338
+
339
+ const result = {};
340
+ const usedNames = new Set();
341
+
342
+ for (let i = 0; i < compiled.segmentNames.length; i++) {
343
+ const segInfo = compiled.segmentNames[i];
344
+ const value = matchResult[segInfo.index + 1] || '';
345
+
346
+ if (usedNames.has(segInfo.name)) {
347
+ if (!Array.isArray(result[segInfo.name])) {
348
+ result[segInfo.name] = [result[segInfo.name]];
349
+ }
350
+ result[segInfo.name].push(value);
351
+ } else {
352
+ usedNames.add(segInfo.name);
353
+ result[segInfo.name] = value;
354
+ }
355
+ }
356
+
357
+ for (const key in result) {
358
+ if (result[key] === '') {
359
+ delete result[key];
360
+ }
361
+ }
362
+
363
+ return result;
364
+ };
365
+
366
+ /**
367
+ * Stringifies a pattern with given values
368
+ * @param {CompiledPattern} compiled - Compiled pattern
369
+ * @param {Object} [values={}] - Values to stringify
370
+ * @returns {string} Generated string
371
+ * @throws {Error} If required values are missing
372
+ */
373
+ const stringify = (compiled, values = {}) => {
374
+ if (compiled.isRegex) {
375
+ throw new Error('Cannot stringify a pattern created from regex');
376
+ }
377
+
378
+ let result = '';
379
+ let i = 0;
380
+
381
+ while (i < compiled.segments.length) {
382
+ const segment = compiled.segments[i];
383
+
384
+ if (segment.optional) {
385
+ let optionalPart = '';
386
+ let j = i;
387
+
388
+ while (j < compiled.segments.length && compiled.segments[j].optional) {
389
+ const seg = compiled.segments[j];
390
+
391
+ if (seg.type === 'literal') {
392
+ optionalPart += seg.name;
393
+ } else if (seg.type === 'named') {
394
+ const val = values[seg.name];
395
+ if (val !== undefined && val !== null && val !== '') {
396
+ optionalPart += Array.isArray(val) ? val.join('/') : val;
397
+ } else {
398
+ optionalPart = '';
399
+ break;
400
+ }
401
+ } else if (seg.type === 'wildcard') {
402
+ const val = values._;
403
+ if (val !== undefined && val !== null && val !== '') {
404
+ optionalPart += Array.isArray(val) ? val.join('/') : val;
405
+ } else {
406
+ optionalPart = '';
407
+ break;
408
+ }
409
+ }
410
+
411
+ j++;
412
+ }
413
+
414
+ if (optionalPart !== '') {
415
+ result += optionalPart;
416
+ }
417
+
418
+ if (i === j) {
419
+ i++;
420
+ } else {
421
+ i = j;
422
+ }
423
+ continue;
424
+ }
425
+
426
+ if (segment.type === 'literal') {
427
+ result += segment.name;
428
+ } else if (segment.type === 'named') {
429
+ const value = values[segment.name];
430
+ if (value === undefined || value === null || value === '') {
431
+ throw new Error(`Missing required value for segment: ${segment.name}`);
432
+ }
433
+ result += Array.isArray(value) ? value.join('/') : value;
434
+ } else if (segment.type === 'wildcard') {
435
+ const value = values._;
436
+ if (value === undefined || value === null || value === '') {
437
+ throw new Error('Missing required wildcard value');
438
+ }
439
+ result += Array.isArray(value) ? value.join('/') : value;
440
+ }
441
+
442
+ i++;
443
+ }
444
+
445
+ return result;
446
+ };
447
+
448
+ /**
449
+ * UrlPattern class for matching and generating URLs
450
+ */
451
+ class UrlPattern {
452
+ /**
453
+ * @param {string|RegExp} pattern - Pattern string or regex
454
+ * @param {UrlPatternOptions|Array<string>} [options={}] - Options or keys (for regex)
455
+ */
456
+ constructor(pattern, options = {}) {
457
+ if (pattern instanceof RegExp) {
458
+ const keys = Array.isArray(options) ? options : [];
459
+ /** @type {CompiledPattern} */
460
+ this.compiled = makePatternFromRegex(pattern, keys);
461
+ } else {
462
+ /** @type {CompiledPattern} */
463
+ this.compiled = makePattern(pattern, /** @type {UrlPatternOptions} */ (options));
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Match a string against the pattern
469
+ * @param {string} str - String to match
470
+ * @returns {Object|null} Extracted values or null if no match
471
+ */
472
+ match(str) {
473
+ return match(this.compiled, str);
474
+ }
475
+
476
+ /**
477
+ * Generate a string from the pattern
478
+ * @param {Object} [values={}] - Values to stringify
479
+ * @returns {string} Generated string
480
+ */
481
+ stringify(values) {
482
+ return stringify(this.compiled, values);
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Creates a new UrlPattern instance (functional API)
488
+ * @param {string|RegExp} pattern - Pattern string or regex
489
+ * @param {UrlPatternOptions|Array<string>} [options={}] - Options or keys
490
+ * @returns {UrlPattern} UrlPattern instance
491
+ */
492
+ const urlPattern = (pattern, options = {}) => {
493
+ return new UrlPattern(pattern, options);
494
+ };
495
+
496
+ exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
497
+ exports.UrlPattern = UrlPattern;
498
+ exports.default = UrlPattern;
499
+ exports.makePattern = makePattern;
500
+ exports.makePatternFromRegex = makePatternFromRegex;
501
+ exports.match = match;
502
+ exports.stringify = stringify;
503
+ exports.urlPattern = urlPattern;