@peter.naydenov/url-pattern 1.0.0
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/index.d.ts +131 -0
- package/dist/index.js +493 -0
- package/package.json +34 -0
- package/readme.md +136 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for customizing the pattern syntax
|
|
3
|
+
*/
|
|
4
|
+
export interface UrlPatternOptions {
|
|
5
|
+
/** Character used for escaping special characters (default: '\\') */
|
|
6
|
+
escapeChar?: string;
|
|
7
|
+
/** Character that starts a named segment (default: ':') */
|
|
8
|
+
segmentNameStartChar?: string;
|
|
9
|
+
/** Characters allowed in segment names (default: 'a-zA-Z0-9') */
|
|
10
|
+
segmentNameCharset?: string;
|
|
11
|
+
/** Characters allowed in segment values (default: 'a-zA-Z0-9-_~ %') */
|
|
12
|
+
segmentValueCharset?: string;
|
|
13
|
+
/** Character that starts an optional segment (default: '(') */
|
|
14
|
+
optionalSegmentStartChar?: string;
|
|
15
|
+
/** Character that ends an optional segment (default: ')') */
|
|
16
|
+
optionalSegmentEndChar?: string;
|
|
17
|
+
/** Character that denotes a wildcard (default: '*') */
|
|
18
|
+
wildcardChar?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Result of matching a pattern against a string
|
|
23
|
+
*/
|
|
24
|
+
export interface MatchResult {
|
|
25
|
+
[key: string]: string | string[] | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compiled pattern object
|
|
30
|
+
*/
|
|
31
|
+
export interface CompiledPattern {
|
|
32
|
+
regex: string;
|
|
33
|
+
regexObj: RegExp;
|
|
34
|
+
segments: ParsedSegment[];
|
|
35
|
+
segmentNames: SegmentName[];
|
|
36
|
+
options: UrlPatternOptions;
|
|
37
|
+
isRegex: boolean;
|
|
38
|
+
pattern?: string;
|
|
39
|
+
keys?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parsed segment
|
|
44
|
+
*/
|
|
45
|
+
export interface ParsedSegment {
|
|
46
|
+
name: string;
|
|
47
|
+
type: 'named' | 'wildcard' | 'literal';
|
|
48
|
+
optional?: boolean;
|
|
49
|
+
regex: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Segment name mapping
|
|
54
|
+
*/
|
|
55
|
+
export interface SegmentName {
|
|
56
|
+
name: string;
|
|
57
|
+
index: number;
|
|
58
|
+
type: 'named' | 'wildcard';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* UrlPattern class for matching and generating URLs
|
|
63
|
+
*/
|
|
64
|
+
export class UrlPattern {
|
|
65
|
+
/**
|
|
66
|
+
* @param pattern - Pattern string or RegExp
|
|
67
|
+
* @param options - Options object or keys array (for regex patterns)
|
|
68
|
+
*/
|
|
69
|
+
constructor(pattern: string | RegExp, options?: UrlPatternOptions | string[]);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Match a string against the pattern
|
|
73
|
+
* @param str - String to match
|
|
74
|
+
* @returns Extracted values or null if no match
|
|
75
|
+
*/
|
|
76
|
+
match(str: string): MatchResult | null;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generate a string from the pattern
|
|
80
|
+
* @param values - Values to stringify
|
|
81
|
+
* @returns Generated string
|
|
82
|
+
*/
|
|
83
|
+
stringify(values?: Record<string, any>): string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a new UrlPattern instance (functional API)
|
|
88
|
+
* @param pattern - Pattern string or RegExp
|
|
89
|
+
* @param options - Options object or keys array (for regex patterns)
|
|
90
|
+
* @returns UrlPattern instance
|
|
91
|
+
*/
|
|
92
|
+
export function urlPattern(pattern: string | RegExp, options?: UrlPatternOptions | string[]): UrlPattern;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Creates a compiled pattern from a string
|
|
96
|
+
* @param pattern - Pattern string
|
|
97
|
+
* @param options - Options
|
|
98
|
+
* @returns Compiled pattern
|
|
99
|
+
*/
|
|
100
|
+
export function makePattern(pattern: string, options?: UrlPatternOptions): CompiledPattern;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates a compiled pattern from a regex
|
|
104
|
+
* @param regex - Regex pattern
|
|
105
|
+
* @param keys - Array of key names for captured groups
|
|
106
|
+
* @returns Compiled pattern
|
|
107
|
+
*/
|
|
108
|
+
export function makePatternFromRegex(regex: RegExp, keys?: string[]): CompiledPattern;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Matches a string against a compiled pattern
|
|
112
|
+
* @param compiled - Compiled pattern
|
|
113
|
+
* @param str - String to match
|
|
114
|
+
* @returns Extracted values or null
|
|
115
|
+
*/
|
|
116
|
+
export function match(compiled: CompiledPattern, str: string): MatchResult | null;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Stringifies a pattern with given values
|
|
120
|
+
* @param compiled - Compiled pattern
|
|
121
|
+
* @param values - Values to stringify
|
|
122
|
+
* @returns Generated string
|
|
123
|
+
*/
|
|
124
|
+
export function stringify(compiled: CompiledPattern, values?: Record<string, any>): string;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Default options
|
|
128
|
+
*/
|
|
129
|
+
export const DEFAULT_OPTIONS: UrlPatternOptions;
|
|
130
|
+
|
|
131
|
+
export default UrlPattern;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview URL pattern matching library
|
|
3
|
+
* @module url-pattern
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} UrlPatternOptions
|
|
8
|
+
* @property {string} [escapeChar='\\'] - Character used for escaping special characters
|
|
9
|
+
* @property {string} [segmentNameStartChar=':'] - Character that starts a named segment
|
|
10
|
+
* @property {string} [segmentNameCharset='a-zA-Z0-9'] - Characters allowed in segment names
|
|
11
|
+
* @property {string} [segmentValueCharset='a-zA-Z0-9-_~ %'] - Characters allowed in segment values
|
|
12
|
+
* @property {string} [optionalSegmentStartChar='('] - Character that starts an optional segment
|
|
13
|
+
* @property {string} [optionalSegmentEndChar=')'] - Character that ends an optional segment
|
|
14
|
+
* @property {string} [wildcardChar='*'] - Character that denotes a wildcard
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} ParsedSegment
|
|
19
|
+
* @property {string} name - Segment name
|
|
20
|
+
* @property {string} type - Segment type ('named' | 'wildcard' | 'literal')
|
|
21
|
+
* @property {boolean} [optional=false] - Whether the segment is optional
|
|
22
|
+
* @property {string} regex - Compiled regex string
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} SegmentName
|
|
27
|
+
* @property {string} name - Segment name
|
|
28
|
+
* @property {number} index - Capture group index
|
|
29
|
+
* @property {string} type - Segment type ('named' | 'wildcard')
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} CompiledPattern
|
|
34
|
+
* @property {string} regex - Compiled regex string
|
|
35
|
+
* @property {RegExp} regexObj - Compiled regex object
|
|
36
|
+
* @property {Array<ParsedSegment>} segments - Parsed segments
|
|
37
|
+
* @property {Array<SegmentName>} segmentNames - Segment name mappings
|
|
38
|
+
* @property {UrlPatternOptions} options - Options used
|
|
39
|
+
* @property {boolean} isRegex - Whether pattern was created from regex
|
|
40
|
+
* @property {string} [pattern] - Original pattern string
|
|
41
|
+
* @property {Array<string>} [keys] - Keys for regex patterns
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default options for URL pattern matching
|
|
46
|
+
* @type {UrlPatternOptions}
|
|
47
|
+
*/
|
|
48
|
+
const DEFAULT_OPTIONS = {
|
|
49
|
+
escapeChar: '\\',
|
|
50
|
+
segmentNameStartChar: ':',
|
|
51
|
+
segmentNameCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
|
|
52
|
+
segmentValueCharset: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_~ %',
|
|
53
|
+
optionalSegmentStartChar: '(',
|
|
54
|
+
optionalSegmentEndChar: ')',
|
|
55
|
+
wildcardChar: '*'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Escapes special regex characters in a string
|
|
60
|
+
* @param {string} str - String to escape
|
|
61
|
+
* @returns {string} Escaped string
|
|
62
|
+
*/
|
|
63
|
+
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Merges default options with user provided options
|
|
67
|
+
* @param {UrlPatternOptions} [userOptions={}] - User provided options
|
|
68
|
+
* @returns {UrlPatternOptions} Merged options
|
|
69
|
+
*/
|
|
70
|
+
const mergeOptions = (userOptions = {}) => ({
|
|
71
|
+
...DEFAULT_OPTIONS,
|
|
72
|
+
...userOptions
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Finds the position of the next special character in the pattern
|
|
77
|
+
* @param {string} pattern - Pattern string
|
|
78
|
+
* @param {number} start - Starting position
|
|
79
|
+
* @param {UrlPatternOptions} options - Parsing options
|
|
80
|
+
* @returns {number} Position of next special character
|
|
81
|
+
*/
|
|
82
|
+
const findNextSpecialChar = (pattern, start, options) => {
|
|
83
|
+
const chars = [
|
|
84
|
+
options.escapeChar,
|
|
85
|
+
options.optionalSegmentStartChar,
|
|
86
|
+
options.optionalSegmentEndChar,
|
|
87
|
+
options.wildcardChar,
|
|
88
|
+
options.segmentNameStartChar
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
let minPos = pattern.length;
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < chars.length; i++) {
|
|
94
|
+
const char = chars[i];
|
|
95
|
+
if (!char) continue;
|
|
96
|
+
const pos = pattern.indexOf(char, start);
|
|
97
|
+
if (pos !== -1 && pos < minPos) {
|
|
98
|
+
minPos = pos;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return minPos;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parses a pattern string into segments
|
|
107
|
+
* @param {string} pattern - Pattern string to parse
|
|
108
|
+
* @param {UrlPatternOptions} options - Parsing options
|
|
109
|
+
* @returns {Array<ParsedSegment>} Parsed segments
|
|
110
|
+
*/
|
|
111
|
+
const parsePattern = (pattern, options) => {
|
|
112
|
+
const segments = [];
|
|
113
|
+
let i = 0;
|
|
114
|
+
let inOptional = false;
|
|
115
|
+
|
|
116
|
+
while (i < pattern.length) {
|
|
117
|
+
const char = pattern[i];
|
|
118
|
+
|
|
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;
|
|
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;
|
|
161
|
+
}
|
|
162
|
+
nameEnd = j + 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const name = remaining.slice(0, nameEnd);
|
|
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 += '-';
|
|
172
|
+
}
|
|
173
|
+
const escapedValueCharset = valueCharset.replace(/\]/g, '\\]');
|
|
174
|
+
const valueRegex = `([${escapedValueCharset}]+)`;
|
|
175
|
+
|
|
176
|
+
segments.push({
|
|
177
|
+
type: 'named',
|
|
178
|
+
name,
|
|
179
|
+
regex: valueRegex,
|
|
180
|
+
optional: inOptional
|
|
181
|
+
});
|
|
182
|
+
i += 1 + nameEnd;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const literalEnd = findNextSpecialChar(pattern, i, options);
|
|
188
|
+
|
|
189
|
+
if (literalEnd > i) {
|
|
190
|
+
const literal = pattern.slice(i, literalEnd);
|
|
191
|
+
segments.push({
|
|
192
|
+
type: 'literal',
|
|
193
|
+
name: literal,
|
|
194
|
+
regex: escapeRegex(literal),
|
|
195
|
+
optional: inOptional
|
|
196
|
+
});
|
|
197
|
+
i = literalEnd;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
i++;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return segments;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Compiles segments into a regex pattern
|
|
209
|
+
* @param {Array<ParsedSegment>} segments - Parsed segments
|
|
210
|
+
* @param {UrlPatternOptions} options - Options
|
|
211
|
+
* @returns {{regex: string, segmentNames: Array<SegmentName>}} Compiled regex and segment names
|
|
212
|
+
*/
|
|
213
|
+
const compileRegex = (segments, options) => {
|
|
214
|
+
let regex = '^';
|
|
215
|
+
let groupIndex = 0;
|
|
216
|
+
/** @type {Array<SegmentName>} */
|
|
217
|
+
const segmentNames = [];
|
|
218
|
+
let i = 0;
|
|
219
|
+
|
|
220
|
+
while (i < segments.length) {
|
|
221
|
+
const segment = segments[i];
|
|
222
|
+
|
|
223
|
+
if (segment.optional) {
|
|
224
|
+
let optionalPart = '';
|
|
225
|
+
let j = i;
|
|
226
|
+
|
|
227
|
+
while (j < segments.length && segments[j].optional) {
|
|
228
|
+
const seg = segments[j];
|
|
229
|
+
|
|
230
|
+
if (seg.type === 'wildcard') {
|
|
231
|
+
optionalPart += '(.*)';
|
|
232
|
+
segmentNames.push({ name: '_', index: groupIndex, type: 'wildcard' });
|
|
233
|
+
groupIndex++;
|
|
234
|
+
} else if (seg.type === 'named') {
|
|
235
|
+
optionalPart += seg.regex;
|
|
236
|
+
segmentNames.push({ name: seg.name, index: groupIndex, type: 'named' });
|
|
237
|
+
groupIndex++;
|
|
238
|
+
} else {
|
|
239
|
+
optionalPart += seg.regex;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
j++;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
regex += `(?:${optionalPart})?`;
|
|
246
|
+
i = j;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (segment.type === 'wildcard') {
|
|
251
|
+
regex += '(.*)';
|
|
252
|
+
segmentNames.push({ name: '_', index: groupIndex, type: 'wildcard' });
|
|
253
|
+
groupIndex++;
|
|
254
|
+
} else if (segment.type === 'named') {
|
|
255
|
+
regex += segment.regex;
|
|
256
|
+
segmentNames.push({ name: segment.name, index: groupIndex, type: 'named' });
|
|
257
|
+
groupIndex++;
|
|
258
|
+
} else {
|
|
259
|
+
regex += segment.regex;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
i++;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
regex += '$';
|
|
266
|
+
|
|
267
|
+
return { regex, segmentNames };
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Creates a compiled pattern from a string
|
|
272
|
+
* @param {string} pattern - Pattern string
|
|
273
|
+
* @param {UrlPatternOptions} [options={}] - Options
|
|
274
|
+
* @returns {CompiledPattern} Compiled pattern
|
|
275
|
+
*/
|
|
276
|
+
const makePattern = (pattern, options = {}) => {
|
|
277
|
+
const mergedOptions = mergeOptions(options);
|
|
278
|
+
const segments = parsePattern(pattern, mergedOptions);
|
|
279
|
+
const { regex, segmentNames } = compileRegex(segments, mergedOptions);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
regex,
|
|
283
|
+
regexObj: new RegExp(regex),
|
|
284
|
+
segments,
|
|
285
|
+
segmentNames,
|
|
286
|
+
options: mergedOptions,
|
|
287
|
+
isRegex: false,
|
|
288
|
+
pattern
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Creates a compiled pattern from a regex
|
|
294
|
+
* @param {RegExp} regex - Regex pattern
|
|
295
|
+
* @param {Array<string>} [keys=[]] - Array of key names for captured groups
|
|
296
|
+
* @returns {CompiledPattern} Compiled pattern
|
|
297
|
+
*/
|
|
298
|
+
const makePatternFromRegex = (regex, keys = []) => {
|
|
299
|
+
return {
|
|
300
|
+
regex: regex.source,
|
|
301
|
+
regexObj: regex,
|
|
302
|
+
segments: [],
|
|
303
|
+
segmentNames: keys.map((name, index) => ({ name, index, type: 'named' })),
|
|
304
|
+
options: DEFAULT_OPTIONS,
|
|
305
|
+
isRegex: true,
|
|
306
|
+
keys
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Matches a string against a compiled pattern
|
|
312
|
+
* @param {CompiledPattern} compiled - Compiled pattern
|
|
313
|
+
* @param {string} str - String to match
|
|
314
|
+
* @returns {Object|null} Extracted values or null if no match
|
|
315
|
+
*/
|
|
316
|
+
const match = (compiled, str) => {
|
|
317
|
+
const matchResult = compiled.regexObj.exec(str);
|
|
318
|
+
|
|
319
|
+
if (!matchResult) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (compiled.isRegex) {
|
|
324
|
+
if (compiled.keys && compiled.keys.length > 0) {
|
|
325
|
+
const result = {};
|
|
326
|
+
compiled.keys.forEach((key, index) => {
|
|
327
|
+
const val = matchResult[index + 1];
|
|
328
|
+
result[key] = val !== undefined ? val : null;
|
|
329
|
+
});
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
return matchResult.slice(1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const result = {};
|
|
336
|
+
const usedNames = new Set();
|
|
337
|
+
|
|
338
|
+
for (let i = 0; i < compiled.segmentNames.length; i++) {
|
|
339
|
+
const segInfo = compiled.segmentNames[i];
|
|
340
|
+
const value = matchResult[segInfo.index + 1] || '';
|
|
341
|
+
|
|
342
|
+
if (usedNames.has(segInfo.name)) {
|
|
343
|
+
if (!Array.isArray(result[segInfo.name])) {
|
|
344
|
+
result[segInfo.name] = [result[segInfo.name]];
|
|
345
|
+
}
|
|
346
|
+
result[segInfo.name].push(value);
|
|
347
|
+
} else {
|
|
348
|
+
usedNames.add(segInfo.name);
|
|
349
|
+
result[segInfo.name] = value;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const key in result) {
|
|
354
|
+
if (result[key] === '') {
|
|
355
|
+
delete result[key];
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return result;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Stringifies a pattern with given values
|
|
364
|
+
* @param {CompiledPattern} compiled - Compiled pattern
|
|
365
|
+
* @param {Object} [values={}] - Values to stringify
|
|
366
|
+
* @returns {string} Generated string
|
|
367
|
+
* @throws {Error} If required values are missing
|
|
368
|
+
*/
|
|
369
|
+
const stringify = (compiled, values = {}) => {
|
|
370
|
+
if (compiled.isRegex) {
|
|
371
|
+
throw new Error('Cannot stringify a pattern created from regex');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let result = '';
|
|
375
|
+
let i = 0;
|
|
376
|
+
|
|
377
|
+
while (i < compiled.segments.length) {
|
|
378
|
+
const segment = compiled.segments[i];
|
|
379
|
+
|
|
380
|
+
if (segment.optional) {
|
|
381
|
+
let optionalPart = '';
|
|
382
|
+
let j = i;
|
|
383
|
+
|
|
384
|
+
while (j < compiled.segments.length && compiled.segments[j].optional) {
|
|
385
|
+
const seg = compiled.segments[j];
|
|
386
|
+
|
|
387
|
+
if (seg.type === 'literal') {
|
|
388
|
+
optionalPart += seg.name;
|
|
389
|
+
} else if (seg.type === 'named') {
|
|
390
|
+
const val = values[seg.name];
|
|
391
|
+
if (val !== undefined && val !== null && val !== '') {
|
|
392
|
+
optionalPart += Array.isArray(val) ? val.join('/') : val;
|
|
393
|
+
} else {
|
|
394
|
+
optionalPart = '';
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
} 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 {
|
|
402
|
+
optionalPart = '';
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
j++;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (optionalPart !== '') {
|
|
411
|
+
result += optionalPart;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (i === j) {
|
|
415
|
+
i++;
|
|
416
|
+
} else {
|
|
417
|
+
i = j;
|
|
418
|
+
}
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
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;
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* UrlPattern class for matching and generating URLs
|
|
446
|
+
*/
|
|
447
|
+
class UrlPattern {
|
|
448
|
+
/**
|
|
449
|
+
* @param {string|RegExp} pattern - Pattern string or regex
|
|
450
|
+
* @param {UrlPatternOptions|Array<string>} [options={}] - Options or keys (for regex)
|
|
451
|
+
*/
|
|
452
|
+
constructor(pattern, options = {}) {
|
|
453
|
+
if (pattern instanceof RegExp) {
|
|
454
|
+
const keys = Array.isArray(options) ? options : [];
|
|
455
|
+
/** @type {CompiledPattern} */
|
|
456
|
+
this.compiled = makePatternFromRegex(pattern, keys);
|
|
457
|
+
} else {
|
|
458
|
+
/** @type {CompiledPattern} */
|
|
459
|
+
this.compiled = makePattern(pattern, /** @type {UrlPatternOptions} */ (options));
|
|
460
|
+
}
|
|
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
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Creates a new UrlPattern instance (functional API)
|
|
484
|
+
* @param {string|RegExp} pattern - Pattern string or regex
|
|
485
|
+
* @param {UrlPatternOptions|Array<string>} [options={}] - Options or keys
|
|
486
|
+
* @returns {UrlPattern} UrlPattern instance
|
|
487
|
+
*/
|
|
488
|
+
const urlPattern = (pattern, options = {}) => {
|
|
489
|
+
return new UrlPattern(pattern, options);
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
export { UrlPattern, urlPattern, makePattern, makePatternFromRegex, match, stringify, DEFAULT_OPTIONS };
|
|
493
|
+
export default UrlPattern;
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@peter.naydenov/url-pattern",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Matching patterns for urls and other strings. Turn strings into data or data into strings.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.esm.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "cp src/index.js dist/index.js",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"cover": "vitest run --coverage",
|
|
16
|
+
"lint": "eslint src --ext .js",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"url",
|
|
21
|
+
"pattern",
|
|
22
|
+
"match",
|
|
23
|
+
"routing",
|
|
24
|
+
"regex"
|
|
25
|
+
],
|
|
26
|
+
"author": "Peter Naydenov",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
30
|
+
"eslint": "^10.3.0",
|
|
31
|
+
"typescript": "^6.0.3",
|
|
32
|
+
"vitest": "^4.1.5"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# URL Pattern (@peter.naydenov/url-pattern)
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@peter.naydenov/url-pattern)
|
|
4
|
+
[](https://github.com/PeterNaydenov/git-url-pattern/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/PeterNaydenov/git-url-pattern/issues)
|
|
6
|
+
[](https://github.com/PeterNaydenov/git-url-pattern)
|
|
7
|
+
[](https://www.npmjs.com/package/@peter.naydenov/url-pattern)
|
|
8
|
+
|
|
9
|
+
Easier than regex string matching patterns for urls and other strings. Turn strings into data or data into strings.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
npm install @peter.naydenov/url-pattern
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Once it has been installed, it can be used by writing this line of JavaScript:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
// if you are using ES6:
|
|
21
|
+
import urlPattern from '@peter.naydenov/url-pattern'
|
|
22
|
+
|
|
23
|
+
// if you are using commonJS:
|
|
24
|
+
const urlPattern = require ( '@peter.naydenov/url-pattern' )
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How to use it
|
|
28
|
+
|
|
29
|
+
### Parse a pattern and match a string
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import urlPattern from '@peter.naydenov/url-pattern'
|
|
33
|
+
|
|
34
|
+
const pattern = urlPattern ( '/user/:username/post/:postId' )
|
|
35
|
+
const result = pattern.match ( '/user/john/post/123' )
|
|
36
|
+
|
|
37
|
+
console.log ( result )
|
|
38
|
+
// Output: { username: 'john', postId: '123' }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Match a URL with optional segments
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import urlPattern from '@peter.naydenov/url-pattern'
|
|
45
|
+
|
|
46
|
+
const pattern = urlPattern ( '/api/(v1)/users/:id' )
|
|
47
|
+
const result = pattern.match ( '/api/v1/users/456' )
|
|
48
|
+
|
|
49
|
+
console.log ( result )
|
|
50
|
+
// Output: { id: '456' }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Use wildcard
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import urlPattern from '@peter.naydenov/url-pattern'
|
|
57
|
+
|
|
58
|
+
const pattern = urlPattern ( '/files/*' )
|
|
59
|
+
const result = pattern.match ( '/files/images/photo.jpg' )
|
|
60
|
+
|
|
61
|
+
console.log ( result )
|
|
62
|
+
// Output: { '*': 'images/photo.jpg' }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Generate URL from data
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import urlPattern from '@peter.naydenov/url-pattern'
|
|
69
|
+
|
|
70
|
+
const pattern = urlPattern ( '/user/:username/post/:postId' )
|
|
71
|
+
const url = pattern.stringify ( { username: 'john', postId: '123' } )
|
|
72
|
+
|
|
73
|
+
console.log ( url )
|
|
74
|
+
// Output: '/user/john/post/123'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Configure pattern options
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import urlPattern from '@peter.naydenov/url-pattern'
|
|
81
|
+
|
|
82
|
+
const pattern = urlPattern ( '/user/{username}/post/{postId}', {
|
|
83
|
+
segmentNameStartChar: '{',
|
|
84
|
+
segmentNameEndChar: '}'
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const result = pattern.match ( '/user/john/post/123' )
|
|
88
|
+
|
|
89
|
+
console.log ( result )
|
|
90
|
+
// Output: { username: 'john', postId: '123' }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## API Reference
|
|
94
|
+
|
|
95
|
+
### `urlPattern(pattern, [options])`
|
|
96
|
+
|
|
97
|
+
Creates a new pattern instance.
|
|
98
|
+
|
|
99
|
+
- **pattern** `{string}` - URL pattern string with named segments (`:name`), optional segments (`(segment)`), or wildcards (`*`)
|
|
100
|
+
- **options** `{object}` - Optional configuration object
|
|
101
|
+
|
|
102
|
+
#### Options
|
|
103
|
+
|
|
104
|
+
- **escapeChar** `{string}` - Character used for escaping special characters (default: `'\\'`)
|
|
105
|
+
- **segmentNameStartChar** `{string}` - Character that starts a named segment (default: `':'`)
|
|
106
|
+
- **segmentNameEndChar** `{string}` - Character that ends a named segment (default: `undefined`)
|
|
107
|
+
- **segmentNameCharset** `{string}` - Characters allowed in segment names (default: `'a-zA-Z0-9'`)
|
|
108
|
+
- **segmentValueCharset** `{string}` - Characters allowed in segment values (default: `'a-zA-Z0-9-_~ %'`)
|
|
109
|
+
- **optionalSegmentStartChar** `{string}` - Character that starts an optional segment (default: `'('`)
|
|
110
|
+
- **optionalSegmentEndChar** `{string}` - Character that ends an optional segment (default: `')'`)
|
|
111
|
+
- **wildcardChar** `{string}` - Character that denotes a wildcard (default: `'*'`)
|
|
112
|
+
|
|
113
|
+
### Pattern Methods
|
|
114
|
+
|
|
115
|
+
#### `pattern.match(string)`
|
|
116
|
+
|
|
117
|
+
Matches a string against the pattern and returns an object with captured values, or `null` if no match.
|
|
118
|
+
|
|
119
|
+
#### `pattern.stringify(data)`
|
|
120
|
+
|
|
121
|
+
Generates a URL string from provided data object.
|
|
122
|
+
|
|
123
|
+
#### `pattern.compile()`
|
|
124
|
+
|
|
125
|
+
Returns an object with `regex` (compiled RegExp), `segments` (parsed segments array), and `segmentNames` (segment name mappings).
|
|
126
|
+
|
|
127
|
+
## Links
|
|
128
|
+
|
|
129
|
+
- [History of changes](CHANGELOG.md)
|
|
130
|
+
- [TypeScript definitions](types/index.d.ts)
|
|
131
|
+
|
|
132
|
+
## Credits
|
|
133
|
+
|
|
134
|
+
'@peter.naydenov/url-pattern' was created and supported by [Peter Naydenov](https://github.com/PeterNaydenov).
|
|
135
|
+
|
|
136
|
+
'@peter.naydenov/url-pattern' is released under the [MIT License](https://github.com/PeterNaydenov/git-url-pattern/blob/main/LICENSE).
|