@fedify/uri-template 2.0.0-pr.475.1 → 2.3.0-dev.1137

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/mod.cjs ADDED
@@ -0,0 +1,1901 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/router/errors.ts
3
+ /**
4
+ * Common base class for router-level errors.
5
+ */
6
+ var RouterError = class extends Error {
7
+ /**
8
+ * @param message Human-readable summary.
9
+ */
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "RouterError";
13
+ }
14
+ };
15
+ /**
16
+ * Raised when a route template is not a path template.
17
+ */
18
+ var RouteTemplatePathError = class extends RouterError {
19
+ template;
20
+ constructor(template) {
21
+ super("Path must start with a slash or a path expansion.");
22
+ this.template = template;
23
+ this.name = "RouteTemplatePathError";
24
+ }
25
+ };
26
+ /**
27
+ * Raised when the same variable name appears in multiple variable
28
+ * specifications whose modifiers imply contradictory `multiple` semantics
29
+ * within a single route template (for example, `{x}` together with `{x*}`).
30
+ */
31
+ var ConflictingVarSpecError = class extends RouterError {
32
+ template;
33
+ variable;
34
+ constructor(template, variable) {
35
+ super(`Variable "${variable}" has conflicting explode/prefix modifiers across the template "${template}".`);
36
+ this.template = template;
37
+ this.variable = variable;
38
+ this.name = "ConflictingVarSpecError";
39
+ }
40
+ };
41
+ /**
42
+ * Raised under the default `exact` route option when the `variables` keys
43
+ * do not exactly match the route template's variables: the set of supplied
44
+ * keys must equal the set of template variables (no unknown keys, none
45
+ * missing). All mismatched names — both unknown and missing — are reported.
46
+ */
47
+ var RouteTemplateOptionsNotMatchedError = class extends RouterError {
48
+ template;
49
+ variable;
50
+ constructor(template, variable) {
51
+ super(`Route options variables do not exactly match the template "${template}"; mismatched: ${variable.map((v) => `"${v}"`).join(", ")}.`);
52
+ this.template = template;
53
+ this.variable = variable;
54
+ this.name = "RouteTemplateOptionsNotMatchedError";
55
+ }
56
+ };
57
+ /**
58
+ * Raised when a variable appears more than once in a route template while
59
+ * its `duplicable` constraint is `false` (the default).
60
+ */
61
+ var DuplicateRouteVariableError = class extends RouterError {
62
+ template;
63
+ variable;
64
+ constructor(template, variable) {
65
+ super(`Variable "${variable}" appears more than once in the template "${template}" but is not marked "duplicable: true".`);
66
+ this.template = template;
67
+ this.variable = variable;
68
+ this.name = "DuplicateRouteVariableError";
69
+ }
70
+ };
71
+ /**
72
+ * Raised when a variable specification uses the explode (`*`) or prefix
73
+ * (`:N`) modifier while the corresponding `explodable`/`prefixable`
74
+ * constraint is `false` (the default).
75
+ */
76
+ var DisallowedVarSpecModifierError = class extends RouterError {
77
+ template;
78
+ variable;
79
+ modifier;
80
+ constructor(template, variable, modifier) {
81
+ super(`Variable "${variable}" uses the ${modifier} modifier in the template "${template}" but is not marked "${modifier === "explode" ? "explodable" : "prefixable"}: true".`);
82
+ this.template = template;
83
+ this.variable = variable;
84
+ this.modifier = modifier;
85
+ this.name = "DisallowedVarSpecModifierError";
86
+ }
87
+ };
88
+ /**
89
+ * Raised when a variable is used with an expression operator that is not in
90
+ * its `operatables` allow-list.
91
+ */
92
+ var DisallowedOperatorError = class extends RouterError {
93
+ template;
94
+ variable;
95
+ operator;
96
+ constructor(template, variable, operator) {
97
+ super(`Variable "${variable}" is used with the operator "${operator}" in the template "${template}", which is not in its "operatables" allow-list.`);
98
+ this.template = template;
99
+ this.variable = variable;
100
+ this.operator = operator;
101
+ this.name = "DisallowedOperatorError";
102
+ }
103
+ };
104
+ //#endregion
105
+ //#region src/template/errors.ts
106
+ /**
107
+ * Errors raised when an RFC 6570 URI template fails to parse or expand.
108
+ *
109
+ * Parse-time hierarchy:
110
+ *
111
+ * ~~~~
112
+ * TemplateParseError
113
+ * ├── UnclosedExpressionError
114
+ * ├── StrayClosingBraceError
115
+ * ├── NestedOpeningBraceError
116
+ * ├── EmptyExpressionError
117
+ * ├── ReservedOperatorError
118
+ * ├── UnknownOperatorError
119
+ * ├── InvalidLiteralError
120
+ * ├── InvalidVarSpecError
121
+ * │ ├── EmptyVarNameError
122
+ * │ ├── InvalidVarNameError
123
+ * │ ├── InvalidPrefixError
124
+ * │ └── TrailingCommaError
125
+ * └── UnexpectedCharacterError
126
+ * ~~~~
127
+ *
128
+ * Expansion-time hierarchy:
129
+ *
130
+ * ~~~~
131
+ * TemplateExpansionError
132
+ * └── PrefixModifierNotApplicableError
133
+ * ~~~~
134
+ *
135
+ * Parse errors carry the original `template` and the 0-based `position` where
136
+ * the offending input was located. Expansion errors carry the runtime variable
137
+ * name whose value cannot be expanded.
138
+ *
139
+ * @module
140
+ */
141
+ /**
142
+ * Common base class for every parse-time error produced by the RFC 6570 parser.
143
+ */
144
+ var TemplateParseError = class extends Error {
145
+ template;
146
+ position;
147
+ hint;
148
+ /**
149
+ * @param template The full URI template string that was being parsed.
150
+ * @param position 0-based index into `template` where the problem was
151
+ * detected. When the offending input spans a range,
152
+ * this is the start of that range.
153
+ * @param hint Short, actionable instruction for the user.
154
+ * @param message Human-readable summary.
155
+ */
156
+ constructor(template, position, hint, message) {
157
+ super(`${message} (at position ${position}): ${hint}`);
158
+ this.template = template;
159
+ this.position = position;
160
+ this.hint = hint;
161
+ this.name = "TemplateParseError";
162
+ }
163
+ };
164
+ /**
165
+ * Raised when an opening `{` has no matching `}` before the template ends.
166
+ *
167
+ * Fix: close the expression with `}` or pct-encode the literal `{` as `%7B`.
168
+ * RFC 6570 does not define an escape syntax.
169
+ */
170
+ var UnclosedExpressionError = class extends TemplateParseError {
171
+ constructor(template, position) {
172
+ super(template, position, "Add the missing '}' to close the expression, or remove the stray '{'.", "Unclosed expression: '{' has no matching '}'");
173
+ this.name = "UnclosedExpressionError";
174
+ }
175
+ };
176
+ /**
177
+ * Raised when a `}` appears outside of any expression.
178
+ *
179
+ * Fix: remove the stray `}` or precede it with a matching `{`.
180
+ */
181
+ var StrayClosingBraceError = class extends TemplateParseError {
182
+ constructor(template, position) {
183
+ super(template, position, "Remove this stray '}' or add a matching '{' before it.", "Stray '}' outside of any expression");
184
+ this.name = "StrayClosingBraceError";
185
+ }
186
+ };
187
+ /**
188
+ * Raised when a `{` appears inside another expression before that expression
189
+ * is closed. RFC 6570 expressions cannot nest.
190
+ *
191
+ * Fix: close the outer expression with `}` before opening a new one.
192
+ */
193
+ var NestedOpeningBraceError = class extends TemplateParseError {
194
+ constructor(template, position) {
195
+ super(template, position, "RFC 6570 expressions cannot nest. Close the outer expression with '}' before starting a new one.", "Nested '{' inside an unclosed expression");
196
+ this.name = "NestedOpeningBraceError";
197
+ }
198
+ };
199
+ /**
200
+ * Raised when a literal section of the template contains a character that is
201
+ * outside the RFC 6570 `literals` set: CTL, SP, `"`, `'`, lone `%`, `<`, `>`,
202
+ * `\\`, `^`, `` ` ``, `|`.
203
+ *
204
+ * Fix: pct-encode the offending character or remove it.
205
+ */
206
+ var InvalidLiteralError = class extends TemplateParseError {
207
+ char;
208
+ constructor(template, position, char) {
209
+ super(template, position, `Pct-encode '${char}' (e.g. '%${char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}') or remove it. Literals may not contain CTL, SP, '\"', '\\'', lone '%', '<', '>', '\\\\', '^', '\`', or '|'.`, `Invalid literal character '${char}'`);
210
+ this.char = char;
211
+ this.name = "InvalidLiteralError";
212
+ }
213
+ };
214
+ /**
215
+ * Raised for `{}` — an expression that contains neither operator nor varspec.
216
+ *
217
+ * Fix: insert at least one varname between the braces, e.g. `{var}`.
218
+ */
219
+ var EmptyExpressionError = class extends TemplateParseError {
220
+ constructor(template, position) {
221
+ super(template, position, "Provide at least one varname inside the braces, e.g. '{var}'.", "Empty expression '{}'");
222
+ this.name = "EmptyExpressionError";
223
+ }
224
+ };
225
+ /**
226
+ * Raised when the operator slot holds one of the characters reserved by
227
+ * RFC 6570 §2.2 for future extensions: `=`, `,`, `!`, `@`, `|`.
228
+ *
229
+ * Fix: drop the reserved operator or replace it with one of the implemented
230
+ * operators (`+`, `#`, `.`, `/`, `;`, `?`, `&`).
231
+ */
232
+ var ReservedOperatorError = class extends TemplateParseError {
233
+ operator;
234
+ constructor(template, position, operator) {
235
+ super(template, position, `Operator '${operator}' is reserved by RFC 6570 §2.2 for future extensions. Use one of '+', '#', '.', '/', ';', '?', '&' instead, or remove the operator.`, `Reserved operator '${operator}'`);
236
+ this.operator = operator;
237
+ this.name = "ReservedOperatorError";
238
+ }
239
+ };
240
+ /**
241
+ * Raised when the operator slot holds a character that is neither a defined
242
+ * RFC 6570 operator nor part of the varname grammar.
243
+ *
244
+ * Fix: use one of the implemented operators (`+`, `#`, `.`, `/`, `;`, `?`,
245
+ * `&`) or remove the character.
246
+ */
247
+ var UnknownOperatorError = class extends TemplateParseError {
248
+ operator;
249
+ constructor(template, position, operator) {
250
+ super(template, position, `Replace '${operator}' with one of '+', '#', '.', '/', ';', '?', '&' or remove it.`, `Unknown operator '${operator}'`);
251
+ this.operator = operator;
252
+ this.name = "UnknownOperatorError";
253
+ }
254
+ };
255
+ /**
256
+ * Common base for malformed varspec errors so users can `instanceof`-filter.
257
+ */
258
+ var InvalidVarSpecError = class extends TemplateParseError {
259
+ varSpec;
260
+ constructor(template, position, hint, message, varSpec) {
261
+ super(template, position, hint, message);
262
+ this.varSpec = varSpec;
263
+ this.name = "InvalidVarSpecError";
264
+ }
265
+ };
266
+ /**
267
+ * Raised when a varspec contains no varname (e.g. `{,foo}` or `{foo,}`).
268
+ */
269
+ var EmptyVarNameError = class extends InvalidVarSpecError {
270
+ constructor(template, position) {
271
+ super(template, position, "Remove the stray comma or insert a varname before/after it.", "Empty varname in variable-list", "");
272
+ this.name = "EmptyVarNameError";
273
+ }
274
+ };
275
+ /**
276
+ * Raised when a varname contains characters outside the RFC 6570 varchar set
277
+ * (`ALPHA / DIGIT / "_" / pct-encoded`, optionally separated by `.`).
278
+ */
279
+ var InvalidVarNameError = class extends InvalidVarSpecError {
280
+ offendingChar;
281
+ constructor(template, position, varSpec, offendingChar) {
282
+ super(template, position, `Varnames may only contain ALPHA, DIGIT, '_', '.', or pct-encoded triplets. Replace '${offendingChar}' or pct-encode it.`, `Invalid character '${offendingChar}' in varname`, varSpec);
283
+ this.offendingChar = offendingChar;
284
+ this.name = "InvalidVarNameError";
285
+ }
286
+ };
287
+ /**
288
+ * Raised when a prefix modifier (`:N`) is malformed: missing digits, leading
289
+ * zero, or `N` outside the range `1..9999`.
290
+ */
291
+ var InvalidPrefixError = class extends InvalidVarSpecError {
292
+ prefix;
293
+ constructor(template, position, varSpec, prefix) {
294
+ super(template, position, "Prefix modifiers must be ':N' where N is a positive integer in 1..9999 with no leading zero (e.g. ':3').", `Invalid prefix modifier ':${prefix}'`, varSpec);
295
+ this.prefix = prefix;
296
+ this.name = "InvalidPrefixError";
297
+ }
298
+ };
299
+ /**
300
+ * Raised when a varspec ends with a trailing comma followed by `}` or end of
301
+ * variable-list (e.g. `{a,b,}`).
302
+ */
303
+ var TrailingCommaError = class extends InvalidVarSpecError {
304
+ constructor(template, position) {
305
+ super(template, position, "Remove the trailing comma, or add a varspec after it.", "Trailing ',' in variable-list", "");
306
+ this.name = "TrailingCommaError";
307
+ }
308
+ };
309
+ /**
310
+ * Raised when an unexpected character appears between a varspec and the next
311
+ * separator (`,` or `}`), e.g. `{a b}` or `{a:3x}`.
312
+ */
313
+ var UnexpectedCharacterError = class extends TemplateParseError {
314
+ char;
315
+ constructor(template, position, char) {
316
+ super(template, position, "Expected ',' or '}' here. Remove the unexpected character or pct-encode it if it belongs in the varname.", `Unexpected character '${char}' in expression`);
317
+ this.char = char;
318
+ this.name = "UnexpectedCharacterError";
319
+ }
320
+ };
321
+ /**
322
+ * Common base class for runtime expansion errors.
323
+ */
324
+ var TemplateExpansionError = class extends Error {
325
+ variableName;
326
+ hint;
327
+ /**
328
+ * @param variableName The variable whose resolved value cannot be expanded.
329
+ * @param hint Short, actionable instruction for the user.
330
+ * @param message Human-readable summary.
331
+ */
332
+ constructor(variableName, hint, message) {
333
+ super(`${message} for '${variableName}': ${hint}`);
334
+ this.variableName = variableName;
335
+ this.hint = hint;
336
+ this.name = "TemplateExpansionError";
337
+ }
338
+ };
339
+ /**
340
+ * Raised when a prefix modifier is applied to a composite value.
341
+ *
342
+ * RFC 6570 §2.4.1 defines prefix modifiers for string values only; lists and
343
+ * associative arrays must use normal or explode expansion instead.
344
+ */
345
+ var PrefixModifierNotApplicableError = class extends TemplateExpansionError {
346
+ prefix;
347
+ valueType;
348
+ constructor(variableName, prefix, valueType) {
349
+ super(variableName, "Remove the prefix modifier from this varspec, or provide a string value instead of a composite value.", `Prefix modifier ':${prefix}' is not applicable to ${valueType} values`);
350
+ this.prefix = prefix;
351
+ this.valueType = valueType;
352
+ this.name = "PrefixModifierNotApplicableError";
353
+ }
354
+ };
355
+ //#endregion
356
+ //#region src/const.ts
357
+ /**
358
+ * Operators implemented by this package, including `""` for simple string
359
+ * expansion with no explicit operator.
360
+ */
361
+ const OPERATORS = [
362
+ "",
363
+ "+",
364
+ ".",
365
+ "/",
366
+ ";",
367
+ "?",
368
+ "&",
369
+ "#"
370
+ ];
371
+ /**
372
+ * RFC 6570 operator behavior table used during expansion.
373
+ * This table is from RFC 6570 Appendix A.
374
+ *
375
+ * | | `NUL` | `+` | `.` | `/` | `;` | `?` | `&` | `#` |
376
+ * | ----- | ------- | ------- | ------- | ------- | ------ | ------ | ------ | ------- |
377
+ * | first | `""` | `""` | `"."` | `"/"` | `";"` | `"?"` | `"&"` | `"#"` |
378
+ * | sep | `","` | `","` | `"."` | `"/"` | `";"` | `"&"` | `"&"` | `","` |
379
+ * | named | `false` | `false` | `false` | `false` | `true` | `true` | `true` | `false` |
380
+ * | ifemp | `""` | `""` | `""` | `""` | `""` | `"="` | `"="` | `""` |
381
+ * | allow | `U` | `U+R` | `U` | `U` | `U` | `U` | `U` | `U+R` |
382
+ */
383
+ const operatorSpecs = {
384
+ "": {
385
+ first: "",
386
+ sep: ",",
387
+ named: false,
388
+ ifEmpty: "",
389
+ allowReserved: false
390
+ },
391
+ "+": {
392
+ first: "",
393
+ sep: ",",
394
+ named: false,
395
+ ifEmpty: "",
396
+ allowReserved: true
397
+ },
398
+ ".": {
399
+ first: ".",
400
+ sep: ".",
401
+ named: false,
402
+ ifEmpty: "",
403
+ allowReserved: false
404
+ },
405
+ "/": {
406
+ first: "/",
407
+ sep: "/",
408
+ named: false,
409
+ ifEmpty: "",
410
+ allowReserved: false
411
+ },
412
+ ";": {
413
+ first: ";",
414
+ sep: ";",
415
+ named: true,
416
+ ifEmpty: "",
417
+ allowReserved: false
418
+ },
419
+ "?": {
420
+ first: "?",
421
+ sep: "&",
422
+ named: true,
423
+ ifEmpty: "=",
424
+ allowReserved: false
425
+ },
426
+ "&": {
427
+ first: "&",
428
+ sep: "&",
429
+ named: true,
430
+ ifEmpty: "=",
431
+ allowReserved: false
432
+ },
433
+ "#": {
434
+ first: "#",
435
+ sep: ",",
436
+ named: false,
437
+ ifEmpty: "",
438
+ allowReserved: true
439
+ }
440
+ };
441
+ //#endregion
442
+ //#region src/template/encoding.ts
443
+ const textEncoder = new TextEncoder();
444
+ const hexDigits = "0123456789ABCDEF";
445
+ /**
446
+ * Returns whether a character is an RFC 3986 hexadecimal digit.
447
+ *
448
+ * Used by parsers and encoders when recognizing pct-encoded triplets.
449
+ */
450
+ const isHexDigit = (char) => some(between(48, 57), between(65, 70), between(97, 102))(char.charCodeAt(0));
451
+ /**
452
+ * Returns whether `value[index]` starts a complete pct-encoded triplet.
453
+ */
454
+ const isPctEncodedAt = (value, index) => value[index] === "%" && index + 2 < value.length && isHexDigit(value[index + 1]) && isHexDigit(value[index + 2]);
455
+ /**
456
+ * Returns the UTF-16 length of an RFC 6570 `varchar` at `index`, or `0` when
457
+ * no varchar starts there.
458
+ */
459
+ function isVarcharAt(value, index) {
460
+ const char = value[index];
461
+ if (char == null) return 0;
462
+ if (some(isAlpha, isDigit$1, eq("_"))(char)) return 1;
463
+ return isPctEncodedAt(value, index) ? 3 : 0;
464
+ }
465
+ /**
466
+ * Returns the UTF-16 length of an RFC 6570 literal token at `index`, or `0`
467
+ * when the character is not valid literal syntax.
468
+ */
469
+ function isLiteralAt(value, index) {
470
+ if (isPctEncodedAt(value, index)) return 3;
471
+ const { char, size } = readCodePoint(value, index);
472
+ return isLiteralChar(char) ? size : 0;
473
+ }
474
+ /**
475
+ * Reads one Unicode code point from a JavaScript string.
476
+ */
477
+ function readCodePoint(value, index) {
478
+ const codePoint = value.codePointAt(index);
479
+ if (codePoint == null) return {
480
+ char: "",
481
+ size: 0
482
+ };
483
+ const size = codePoint > 65535 ? 2 : 1;
484
+ return {
485
+ char: value.slice(index, index + size),
486
+ size
487
+ };
488
+ }
489
+ /**
490
+ * Percent-encodes an expanded variable value according to the operator's
491
+ * allowed-character rule.
492
+ */
493
+ const encodeValue = (allowReserved) => (value) => {
494
+ let encoded = "";
495
+ for (let index = 0; index < value.length;) {
496
+ if (allowReserved && isPctEncodedAt(value, index)) {
497
+ encoded += value.slice(index, index + 3);
498
+ index += 3;
499
+ continue;
500
+ }
501
+ const { char, size } = readCodePoint(value, index);
502
+ encoded += some(isUnreserved, (char) => allowReserved && isReserved(char))(char) ? char : percentEncode(char);
503
+ index += size;
504
+ }
505
+ return encoded;
506
+ };
507
+ /**
508
+ * Percent-encodes a variable name or associative key for named expansions.
509
+ */
510
+ function encodeName(value) {
511
+ let encoded = "";
512
+ for (let index = 0; index < value.length;) {
513
+ if (isPctEncodedAt(value, index)) {
514
+ encoded += value.slice(index, index + 3);
515
+ index += 3;
516
+ continue;
517
+ }
518
+ const { char, size } = readCodePoint(value, index);
519
+ encoded += isUnreserved(char) ? char : percentEncode(char);
520
+ index += size;
521
+ }
522
+ return encoded;
523
+ }
524
+ /**
525
+ * Returns the first `length` RFC 6570 prefix characters without splitting a
526
+ * Unicode code point or a pct-encoded triplet.
527
+ */
528
+ function truncateValue(value, length) {
529
+ let truncated = "";
530
+ let count = 0;
531
+ for (let index = 0; index < value.length && count < length; count++) {
532
+ if (isPctEncodedAt(value, index)) {
533
+ truncated += value.slice(index, index + 3);
534
+ index += 3;
535
+ continue;
536
+ }
537
+ const { char, size } = readCodePoint(value, index);
538
+ truncated += char;
539
+ index += size;
540
+ }
541
+ return truncated;
542
+ }
543
+ const isAlpha = (char) => some(between(65, 90), between(97, 122))(char.charCodeAt(0));
544
+ const isDigit$1 = (char) => between(48, 57)(char.charCodeAt(0));
545
+ const isUnreserved = (char) => some(isAlpha, isDigit$1, (char) => "-._~".includes(char))(char);
546
+ const isReserved = (char) => ":/?#[]@!$&'()*+,;=".includes(char);
547
+ const isLiteralChar = (char) => isLiteralCodePoint(char.codePointAt(0));
548
+ const isLiteralCodePoint = (code) => code != null && some(eq(33), between(35, 36), eq(38), between(40, 59), eq(61), between(63, 91), eq(93), eq(95), between(97, 122), eq(126), isUcsChar, isIPrivate)(code);
549
+ function isUcsChar(code) {
550
+ if (code < 65536) return some(between(160, 55295), between(63744, 64975), between(65008, 65519))(code);
551
+ if (code > 983037) return false;
552
+ const offset = code % 65536;
553
+ return offset <= 65533 && (code < 917504 || offset >= 4096);
554
+ }
555
+ const isIPrivate = (code) => some(between(57344, 63743), between(983040, 1048573), between(1048576, 1114109))(code);
556
+ const percentEncode = (char) => Array.from(textEncoder.encode(char)).map((byte) => `%${hexDigits[byte >> 4]}${hexDigits[byte & 15]}`).join("");
557
+ const between = (min, max) => (num) => min <= num && num <= max;
558
+ const eq = (a) => (b) => a === b;
559
+ const some = (...preds) => (arg) => preds.some((pred) => pred(arg));
560
+ //#endregion
561
+ //#region src/template/expand.ts
562
+ /**
563
+ * Expands one parsed URI Template expression against the supplied variable
564
+ * context using the operator behavior table from RFC 6570.
565
+ */
566
+ function expand(tokens, context, options) {
567
+ return tokens.map((token) => token.kind === "literal" ? token.text : expandExpressions(token.vars, token.operator, context, options)).join("");
568
+ }
569
+ function expandExpressions(vars, operator, context, options) {
570
+ const spec = operatorSpecs[operator];
571
+ const parts = vars.flatMap((varSpec) => expandValue(varSpec, context[varSpec.name], spec, options));
572
+ return parts.length < 1 ? "" : `${spec.first}${parts.join(spec.sep)}`;
573
+ }
574
+ function expandValue(varSpec, value, spec, options) {
575
+ if (value == null) return [];
576
+ if (isPrimitiveList(value)) {
577
+ const encoded = encodeListMembers(value, spec.allowReserved);
578
+ if (encoded.length < 1) return [];
579
+ if (!reportPrefixModifierError(varSpec, "list", options)) return [];
580
+ return expandList(varSpec, encoded, spec);
581
+ }
582
+ if (isAssociative$1(value)) {
583
+ const pairs = encodeAssociativePairs(value, spec.allowReserved);
584
+ if (pairs.length < 1) return [];
585
+ if (!reportPrefixModifierError(varSpec, "associative", options)) return [];
586
+ return expandAssociative(varSpec, pairs, spec);
587
+ }
588
+ return expandPrimitive(varSpec, value, spec);
589
+ }
590
+ function expandPrimitive(varSpec, value, spec) {
591
+ const text = String(value);
592
+ const prefixed = varSpec.prefix == null ? text : truncateValue(text, varSpec.prefix);
593
+ const encoded = encodeValue(spec.allowReserved)(prefixed);
594
+ if (!spec.named) return [encoded];
595
+ return [expandNamedPair(encodeName(varSpec.name), encoded, spec)];
596
+ }
597
+ function expandList(varSpec, encoded, spec) {
598
+ const name = encodeName(varSpec.name);
599
+ if (varSpec.explode) return spec.named ? encoded.map((item) => expandNamedPair(name, item, spec)) : [...encoded];
600
+ const joined = encoded.join(",");
601
+ return spec.named ? [expandNamedPair(name, joined, spec)] : [joined];
602
+ }
603
+ function expandAssociative(varSpec, pairs, spec) {
604
+ if (varSpec.explode) return pairs.map(([key, item]) => expandNamedPair(key, item, spec));
605
+ const item = pairs.flat(1).join(",");
606
+ if (!spec.named) return [item];
607
+ return [expandNamedPair(encodeName(varSpec.name), item, spec)];
608
+ }
609
+ const expandNamedPair = (key, item, spec) => item === "" ? `${key}${spec.ifEmpty}` : `${key}=${item}`;
610
+ const encodeListMembers = (value, allowReserved) => value.filter((item) => item != null).map(String).map(encodeValue(allowReserved));
611
+ const encodeAssociativePairs = (value, allowReserved) => Object.entries(value).map(([key, item]) => [key, normalizePairValue(item)]).filter(([, normalized]) => normalized != null).map((kv) => kv.map(encodeValue(allowReserved)));
612
+ function normalizePairValue(value) {
613
+ if (value == null) return null;
614
+ if (!Array.isArray(value)) return String(value);
615
+ const items = value.filter((item) => item != null).map(String);
616
+ return items.length < 1 ? null : items.join(",");
617
+ }
618
+ const isAssociative$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
619
+ const isPrimitiveList = (value) => Array.isArray(value);
620
+ function reportPrefixModifierError(varSpec, valueType, { report, strict }) {
621
+ if (varSpec.prefix == null) return true;
622
+ const error = new PrefixModifierNotApplicableError(varSpec.name, varSpec.prefix, valueType);
623
+ report(error);
624
+ if (strict) throw error;
625
+ return false;
626
+ }
627
+ //#endregion
628
+ //#region src/template/match.ts
629
+ /**
630
+ * Matches a URI against a parsed URI template and extracts variable bindings.
631
+ *
632
+ * The inverse of {@link expand}: given the token stream produced by parsing a
633
+ * template and a concrete URI, recovers an {@link ExpandContext} such that
634
+ * re-expanding the template with that context reproduces the original URI.
635
+ *
636
+ * @param tokens The parsed template tokens to match against.
637
+ * @param uri The concrete URI to decompose.
638
+ * @param options Template options shared with the expansion side.
639
+ * @returns The recovered variable context, or `null` if the URI does not match
640
+ * the template under any interpretation.
641
+ */
642
+ function match(tokens, uri, options) {
643
+ return matchTokens(tokens, uri, options, 0, 0, {});
644
+ }
645
+ /**
646
+ * Walks the token stream and the URI in lockstep, backtracking over every
647
+ * candidate decomposition until one survives roundtrip verification.
648
+ *
649
+ * Literal tokens advance deterministically; expression tokens fan out across
650
+ * all viable end positions and value interpretations. When the token stream is
651
+ * exhausted, the accumulated bindings are accepted only if re-expanding the
652
+ * template with them yields the original URI exactly — this is what filters
653
+ * out the spurious interpretations that the permissive parsing stage admits.
654
+ *
655
+ * @returns The first surviving context, or `null` if every branch fails.
656
+ */
657
+ function matchTokens(tokens, uri, options, tokenIndex, uriIndex, bindings) {
658
+ if (tokenIndex >= tokens.length) {
659
+ if (uriIndex !== uri.length) return null;
660
+ const context = toExpandContext(bindings);
661
+ return expand(tokens, context, options) === uri ? context : null;
662
+ }
663
+ const token = tokens[tokenIndex];
664
+ if (token.kind === "literal") return uri.startsWith(token.text, uriIndex) ? matchTokens(tokens, uri, options, tokenIndex + 1, uriIndex + token.text.length, bindings) : null;
665
+ for (const end of expressionEnds(tokens, uri, tokenIndex, uriIndex)) {
666
+ const expression = uri.slice(uriIndex, end);
667
+ for (const expressionBindings of matchExpression(token.vars, token.operator, expression)) {
668
+ const merged = mergeBindings(bindings, expressionBindings);
669
+ if (merged == null) continue;
670
+ const result = matchTokens(tokens, uri, options, tokenIndex + 1, end, merged);
671
+ if (result != null) return result;
672
+ }
673
+ }
674
+ return null;
675
+ }
676
+ /**
677
+ * Generates candidate end positions in the URI for the expression token at
678
+ * `tokenIndex`, using the next non-empty literal token as a search anchor.
679
+ *
680
+ * If no following literal exists the expression may run to the end of the URI,
681
+ * so every position from the URI end back to `uriIndex` is yielded (longest
682
+ * first, biasing the search toward greedy matches). Otherwise only the offsets
683
+ * where the next literal appears are returned, which prunes the search space
684
+ * dramatically in templates with structural separators.
685
+ */
686
+ function* expressionEnds(tokens, uri, tokenIndex, uriIndex) {
687
+ const nextLiteral = tokens.slice(tokenIndex + 1).find((token) => token.kind === "literal" && token.text !== "");
688
+ if (nextLiteral == null || nextLiteral.kind !== "literal") {
689
+ yield* range(uri.length, uriIndex);
690
+ return;
691
+ }
692
+ for (let index = uri.indexOf(nextLiteral.text, uriIndex); index >= 0; index = uri.indexOf(nextLiteral.text, index + 1)) yield index;
693
+ }
694
+ function* range(from, to) {
695
+ for (let value = from; value >= to; value--) yield value;
696
+ }
697
+ /**
698
+ * Decomposes a single expression substring into every plausible binding set.
699
+ *
700
+ * Validates the operator's leading sigil (`?`, `#`, `/`, etc.), strips it, and
701
+ * dispatches to the named or unnamed parser based on the operator spec. The
702
+ * empty-expression case is delegated to {@link matchEmptyExpression}, which
703
+ * decides when an empty expression substring may be read back as an
704
+ * empty-string binding rather than as no binding at all.
705
+ */
706
+ function* matchExpression(vars, operator, expression) {
707
+ if (expression === "") {
708
+ yield* matchEmptyExpression(vars, operator);
709
+ return;
710
+ }
711
+ const spec = operatorSpecs[operator];
712
+ if (!expression.startsWith(spec.first)) return;
713
+ const body = expression.slice(spec.first.length);
714
+ yield* spec.named ? matchNamedExpression(vars, spec, body) : matchUnnamedExpression(vars, spec, body);
715
+ }
716
+ const matchEmptyExpression = (vars, operator) => {
717
+ if ((operator === "" || operator === "+") && vars.length === 1) return [bindValue(vars[0], "")];
718
+ return [{}];
719
+ };
720
+ const matchUnnamedExpression = (vars, spec, body) => matchUnnamedFrom(vars, spec, split(body, spec.sep), 0, 0);
721
+ /**
722
+ * Distributes the separator-split parts of an unnamed expression across the
723
+ * remaining variables via backtracking.
724
+ *
725
+ * For each variable, every contiguous slice of parts that respects the
726
+ * `minLength`/`maxLength` budget is tried as that variable's value, and the
727
+ * variable may also be skipped entirely (consuming zero parts) to handle
728
+ * undefined variables in the template. Surviving combinations are yielded for
729
+ * the caller to filter.
730
+ */
731
+ function* matchUnnamedFrom(vars, spec, parts, varIndex, partIndex) {
732
+ if (varIndex >= vars.length) {
733
+ if (partIndex >= parts.length) yield {};
734
+ return;
735
+ }
736
+ const varSpec = vars[varIndex];
737
+ for (const consumed of consumeUnnamed(varSpec, spec, parts, partIndex)) for (const rest of matchUnnamedFrom(vars, spec, parts, varIndex + 1, consumed.index)) {
738
+ const merged = mergeBindings(consumed.bindings, rest);
739
+ if (merged != null) yield merged;
740
+ }
741
+ yield* matchUnnamedFrom(vars, spec, parts, varIndex + 1, partIndex);
742
+ }
743
+ /**
744
+ * Enumerates every (binding, next-part-index) pair produced by letting one
745
+ * unnamed variable consume any number of remaining parts.
746
+ */
747
+ function* consumeUnnamed(varSpec, spec, parts, partIndex) {
748
+ if (partIndex >= parts.length) return;
749
+ const maxLength = parts.length - partIndex;
750
+ for (let length = 1; length <= maxLength; length++) {
751
+ const slice = parts.slice(partIndex, partIndex + length);
752
+ for (const bindings of parseUnnamedValue(varSpec, spec, slice)) yield {
753
+ bindings,
754
+ index: partIndex + length
755
+ };
756
+ }
757
+ }
758
+ /**
759
+ * Yields every binding interpretation of a slice assigned to one unnamed
760
+ * variable: scalar, comma-list, associative, and (for explode) an
761
+ * exploded list or associative reading of the same parts.
762
+ *
763
+ * Prefix-bound variables collapse to the scalar reading only.
764
+ */
765
+ function* parseUnnamedValue(varSpec, spec, parts) {
766
+ const nonExploded = parseNonExplodedValue(varSpec, spec, parts.join(spec.sep));
767
+ if (varSpec.prefix != null) {
768
+ yield* nonExploded;
769
+ return;
770
+ }
771
+ if (varSpec.explode && parts.length > 0) yield* parseExplodedUnnamed(varSpec, spec, parts);
772
+ yield* nonExploded;
773
+ }
774
+ function* parseExplodedUnnamed(varSpec, spec, parts) {
775
+ const decodedList = decodeValues(parts, spec.allowReserved);
776
+ if (decodedList == null) return;
777
+ const object = parseExplodedAssociative(parts, spec);
778
+ if (object != null) yield bindValue(varSpec, object);
779
+ yield bindValue(varSpec, decodedList);
780
+ }
781
+ const parseExplodedAssociative = (parts, spec) => parseExplodedAssociativeBody(parts.join(spec.sep), spec);
782
+ function parseExplodedAssociativeBody(body, spec) {
783
+ const entries = [];
784
+ for (let index = 0; index < body.length;) {
785
+ const equals = body.indexOf("=", index);
786
+ if (equals < 0) return null;
787
+ const valueStart = equals + 1;
788
+ const valueEnd = findExplodedPairBoundary(body, valueStart, spec.sep);
789
+ const key = decodeValue(body.slice(index, equals), spec.allowReserved);
790
+ const value = decodeValue(body.slice(valueStart, valueEnd), spec.allowReserved);
791
+ if (key == null || value == null) return null;
792
+ entries.push([key, value]);
793
+ index = valueEnd + spec.sep.length;
794
+ }
795
+ return entries.length < 1 ? null : Object.fromEntries(entries);
796
+ }
797
+ function findExplodedPairBoundary(body, start, separator) {
798
+ for (let index = start; index < body.length; index++) if (isExplodedPairBoundary(body, index, separator)) return index;
799
+ return body.length;
800
+ }
801
+ const isExplodedPairBoundary = (body, index, separator) => {
802
+ if (!body.startsWith(separator, index)) return false;
803
+ const keyStart = index + separator.length;
804
+ for (let i = keyStart; i < body.length; i++) {
805
+ if (body[i] === "=") return i > keyStart;
806
+ if (body.startsWith(separator, i)) return false;
807
+ }
808
+ return false;
809
+ };
810
+ /**
811
+ * Yields candidate readings of a single value string under non-exploded
812
+ * encoding: scalar, comma-separated list, and (when an even element count
813
+ * permits) comma-separated associative array. Some candidates will not
814
+ * round-trip — for instance a comma-bearing scalar gets re-encoded with
815
+ * `%2C` on expansion — and are filtered out by the roundtrip check in
816
+ * {@link matchTokens}.
817
+ *
818
+ * The yielded bindings are ordered from most structured to least, so the
819
+ * surrounding backtracking tries the richer interpretations before the scalar
820
+ * one.
821
+ */
822
+ function* parseNonExplodedValue(varSpec, spec, value) {
823
+ const primitive = decodeValue(value, spec.allowReserved);
824
+ if (primitive == null) return;
825
+ const primitiveBinding = bindValue(varSpec, primitive);
826
+ if (varSpec.prefix != null) {
827
+ yield primitiveBinding;
828
+ return;
829
+ }
830
+ const commaParts = split(value, ",");
831
+ if (commaParts.length < 2) {
832
+ yield primitiveBinding;
833
+ return;
834
+ }
835
+ const decodedList = decodeValues(commaParts, spec.allowReserved);
836
+ if (decodedList == null) {
837
+ yield primitiveBinding;
838
+ return;
839
+ }
840
+ const associative = commaParts.length % 2 === 0 ? parseAssociative(commaParts, spec.allowReserved) : null;
841
+ if (associative != null) yield bindValue(varSpec, associative);
842
+ yield bindValue(varSpec, decodedList);
843
+ yield primitiveBinding;
844
+ }
845
+ const matchNamedExpression = (vars, spec, body) => matchNamedFrom(vars, spec, split(body, spec.sep).map(splitNamedPart), 0, 0);
846
+ /**
847
+ * Named-expression counterpart of {@link matchUnnamedFrom}: backtracks over
848
+ * `name=value` parts assigning them to declared variables.
849
+ *
850
+ * Like the unnamed variant, each variable may either consume a contiguous run
851
+ * of parts (via {@link consumeNamed}) or be skipped entirely so that variables
852
+ * absent from the URI remain unbound.
853
+ */
854
+ function* matchNamedFrom(vars, spec, parts, varIndex, partIndex) {
855
+ if (varIndex >= vars.length) {
856
+ if (partIndex >= parts.length) yield {};
857
+ return;
858
+ }
859
+ const varSpec = vars[varIndex];
860
+ for (const consumed of consumeNamed(varSpec, spec, parts, partIndex, vars)) for (const rest of matchNamedFrom(vars, spec, parts, varIndex + 1, consumed.index)) {
861
+ const merged = mergeBindings(consumed.bindings, rest);
862
+ if (merged != null) yield merged;
863
+ }
864
+ yield* matchNamedFrom(vars, spec, parts, varIndex + 1, partIndex);
865
+ }
866
+ function* consumeNamed(varSpec, spec, parts, partIndex, vars) {
867
+ if (partIndex >= parts.length) return;
868
+ if (varSpec.explode && varSpec.prefix == null) {
869
+ yield* consumeExplodedNamed(varSpec, spec, parts, partIndex, vars);
870
+ return;
871
+ }
872
+ yield* consumeNamedValue(varSpec, spec, parts, partIndex);
873
+ }
874
+ function* consumeNamedValue(varSpec, spec, parts, partIndex) {
875
+ const part = parts[partIndex];
876
+ if (part.name !== encodeName(varSpec.name)) return;
877
+ for (const bindings of parseNonExplodedValue(varSpec, spec, part.value)) yield {
878
+ bindings,
879
+ index: partIndex + 1
880
+ };
881
+ }
882
+ function* consumeExplodedNamed(varSpec, spec, parts, partIndex, vars) {
883
+ yield* consumeNamedList(varSpec, spec, parts, partIndex);
884
+ yield* consumeNamedAssociative(varSpec, spec, parts, partIndex, vars);
885
+ }
886
+ /**
887
+ * Reads consecutive parts that share the variable's name, decoding each as a
888
+ * list element. Used for the explode-as-list interpretation of a named
889
+ * variable.
890
+ */
891
+ function* consumeNamedList(varSpec, spec, parts, partIndex) {
892
+ const values = [...namedListValues(encodeName(varSpec.name), spec, parts, partIndex)];
893
+ if (values.length > 0) yield {
894
+ bindings: bindValue(varSpec, values),
895
+ index: partIndex + values.length
896
+ };
897
+ }
898
+ function* namedListValues(name, spec, parts, partIndex) {
899
+ for (let index = partIndex; index < parts.length && parts[index].name === name; index++) {
900
+ const value = decodeValue(parts[index].value, spec.allowReserved);
901
+ if (value == null) return;
902
+ yield value;
903
+ }
904
+ }
905
+ /**
906
+ * Reads consecutive parts as `key=value` entries of an associative array under
907
+ * one named exploded variable.
908
+ *
909
+ * After the first part, stops as soon as a part's name matches any declared
910
+ * variable so that those parts remain available for their own variables. The
911
+ * first part is always consumed regardless of name to bootstrap the
912
+ * association.
913
+ */
914
+ function* consumeNamedAssociative(varSpec, spec, parts, partIndex, vars) {
915
+ const entries = [...namedAssociativeEntries(spec, parts, partIndex, new Set(vars.map((item) => encodeName(item.name))))];
916
+ if (entries.length > 0) yield {
917
+ bindings: bindValue(varSpec, Object.fromEntries(entries)),
918
+ index: partIndex + entries.length
919
+ };
920
+ }
921
+ function* namedAssociativeEntries(spec, parts, partIndex, reservedNames) {
922
+ for (let index = partIndex; index < parts.length; index++) {
923
+ const part = parts[index];
924
+ if (index > partIndex && reservedNames.has(part.name)) return;
925
+ const key = decodeValue(part.name, spec.allowReserved);
926
+ const value = decodeValue(part.value, spec.allowReserved);
927
+ if (key == null || value == null) return;
928
+ yield [key, value];
929
+ }
930
+ }
931
+ function splitNamedPart(part) {
932
+ const equals = part.indexOf("=");
933
+ return equals < 0 ? {
934
+ name: part,
935
+ value: ""
936
+ } : {
937
+ name: part.slice(0, equals),
938
+ value: part.slice(equals + 1)
939
+ };
940
+ }
941
+ function parseAssociative(parts, allowReserved) {
942
+ const entries = [];
943
+ for (let index = 0; index < parts.length; index += 2) {
944
+ const key = decodeValue(parts[index], allowReserved);
945
+ const value = decodeValue(parts[index + 1], allowReserved);
946
+ if (key == null || value == null) return null;
947
+ entries.push([key, value]);
948
+ }
949
+ return Object.fromEntries(entries);
950
+ }
951
+ function decodeValues(values, allowReserved) {
952
+ const decoded = [];
953
+ for (const value of values) {
954
+ const item = decodeValue(value, allowReserved);
955
+ if (item == null) return null;
956
+ decoded.push(item);
957
+ }
958
+ return decoded;
959
+ }
960
+ function decodeValue(value, allowReserved) {
961
+ if (allowReserved) return value;
962
+ try {
963
+ return decodeURIComponent(value);
964
+ } catch {
965
+ return null;
966
+ }
967
+ }
968
+ const bindValue = ({ name, prefix }, value) => ({ [name]: {
969
+ value,
970
+ prefix
971
+ } });
972
+ /**
973
+ * Combines two binding sets, returning `null` if any shared variable receives
974
+ * incompatible values across them.
975
+ *
976
+ * Equality of values and prefix-aware compatibility (one binding being a
977
+ * truncation of the other) are both delegated to {@link mergeBinding}.
978
+ */
979
+ function mergeBindings(left, right) {
980
+ const merged = { ...left };
981
+ for (const [name, binding] of Object.entries(right)) {
982
+ const existing = merged[name];
983
+ if (existing == null) {
984
+ merged[name] = binding;
985
+ continue;
986
+ }
987
+ const next = mergeBinding(existing, binding);
988
+ if (next == null) return null;
989
+ merged[name] = next;
990
+ }
991
+ return merged;
992
+ }
993
+ const mergeBinding = (left, right) => isPrefixBinding(left) || isPrefixBinding(right) ? mergePrefixBinding(left, right) : equalExpandValue(left.value, right.value) ? left : null;
994
+ /**
995
+ * Reconciles two bindings when at least one carries a prefix limit, by
996
+ * checking that the truncation of the longer (or unrestricted) value matches
997
+ * the shorter prefixed value.
998
+ *
999
+ * Returns the binding that carries the more complete information, or `null`
1000
+ * when no consistent reading exists or non-string values are involved.
1001
+ */
1002
+ function mergePrefixBinding(left, right) {
1003
+ const leftValue = primitiveString(left.value);
1004
+ const rightValue = primitiveString(right.value);
1005
+ if (leftValue == null || rightValue == null) return null;
1006
+ const isLeftPrefix = isPrefixBinding(left);
1007
+ const isRightPrefix = isPrefixBinding(right);
1008
+ if (!isLeftPrefix && !isRightPrefix) return null;
1009
+ if (isLeftPrefix && !isRightPrefix) return truncateValue(rightValue, left.prefix) === leftValue ? right : null;
1010
+ if (!isLeftPrefix && isRightPrefix) return truncateValue(leftValue, right.prefix) === rightValue ? left : null;
1011
+ const leftPrefix = left.prefix;
1012
+ const rightPrefix = right.prefix;
1013
+ if (leftPrefix <= rightPrefix) return truncateValue(rightValue, leftPrefix) === leftValue ? right : null;
1014
+ return truncateValue(leftValue, rightPrefix) === rightValue ? left : null;
1015
+ }
1016
+ const isPrefixBinding = (binding) => binding.prefix != null;
1017
+ const primitiveString = (value) => typeof value === "string" ? value : null;
1018
+ function equalExpandValue(left, right) {
1019
+ if (Array.isArray(left)) return Array.isArray(right) && left.length === right.length && left.every((item, index) => item === right[index]);
1020
+ if (isAssociative(left)) return isAssociative(right) && equalEntries(Object.entries(left), Object.entries(right));
1021
+ return left === right;
1022
+ }
1023
+ const equalEntries = (left, right) => left.length === right.length && left.every(([key, value], index) => {
1024
+ const [rightKey, rightValue] = right[index];
1025
+ return key === rightKey && value === rightValue;
1026
+ });
1027
+ const isAssociative = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1028
+ const toExpandContext = (bindings) => Object.fromEntries(Object.entries(bindings).map(([key, binding]) => [key, binding.value]));
1029
+ const split = (value, separator) => separator === "" ? [value] : value.split(separator);
1030
+ //#endregion
1031
+ //#region src/template/expression.ts
1032
+ const reservedOperators = [
1033
+ "=",
1034
+ ",",
1035
+ "!",
1036
+ "@",
1037
+ "|"
1038
+ ];
1039
+ /**
1040
+ * Parses the content between `{` and `}` into one expression token.
1041
+ *
1042
+ * The tokenizer supplies the original template and offset so errors can point
1043
+ * at the original source string.
1044
+ */
1045
+ function parseExpression(source, template, position) {
1046
+ const raiseExpressionError = (error) => {
1047
+ throw error;
1048
+ };
1049
+ if (source.length < 1) return raiseExpressionError(new EmptyExpressionError(template, position));
1050
+ const first = source[0];
1051
+ if (isReservedOperator(first)) return raiseExpressionError(new ReservedOperatorError(template, position + 1, first));
1052
+ if (!isOperator(first) && isVarcharAt(source, 0) < 1) return raiseExpressionError(new UnknownOperatorError(template, position + 1, first));
1053
+ const operator = isOperator(first) ? first : "";
1054
+ const varListStart = operator === "" ? 0 : 1;
1055
+ return {
1056
+ kind: "expression",
1057
+ operator,
1058
+ vars: parseVarList(source, template, position + 1, varListStart)
1059
+ };
1060
+ }
1061
+ function parseVarList(source, template, offset, start) {
1062
+ if (start >= source.length) throw new EmptyVarNameError(template, offset + start);
1063
+ const vars = [];
1064
+ for (let index = start; index < source.length;) {
1065
+ if (source[index] === ",") throw index === source.length - 1 ? new TrailingCommaError(template, offset + index) : new EmptyVarNameError(template, offset + index);
1066
+ const varStart = index;
1067
+ const nameEnd = readVarNameEnd(source, index);
1068
+ if (nameEnd === varStart) throw new EmptyVarNameError(template, offset + index);
1069
+ const name = source.slice(varStart, nameEnd);
1070
+ index = nameEnd;
1071
+ const modifier = readModifier(source, template, offset, index, name);
1072
+ index = modifier.index;
1073
+ if (index < source.length && source[index] !== ",") throw modifier.used ? new UnexpectedCharacterError(template, offset + index, source[index]) : new InvalidVarNameError(template, offset + index, name, source[index]);
1074
+ vars.push({
1075
+ name,
1076
+ explode: modifier.explode,
1077
+ ...modifier.prefix == null ? {} : { prefix: modifier.prefix }
1078
+ });
1079
+ if (index < source.length) {
1080
+ index++;
1081
+ if (index >= source.length) throw new TrailingCommaError(template, offset + index - 1);
1082
+ }
1083
+ }
1084
+ return vars;
1085
+ }
1086
+ function readVarNameEnd(source, start) {
1087
+ let index = start;
1088
+ let expectVarchar = true;
1089
+ while (index < source.length) {
1090
+ const varcharLength = isVarcharAt(source, index);
1091
+ if (varcharLength > 0) {
1092
+ index += varcharLength;
1093
+ expectVarchar = false;
1094
+ continue;
1095
+ }
1096
+ if (source[index] !== ".") break;
1097
+ if (expectVarchar || isVarcharAt(source, index + 1) < 1) break;
1098
+ index++;
1099
+ expectVarchar = true;
1100
+ }
1101
+ return index;
1102
+ }
1103
+ function readModifier(source, template, offset, index, varSpec) {
1104
+ if (source[index] === "*") return {
1105
+ explode: true,
1106
+ index: index + 1,
1107
+ used: true
1108
+ };
1109
+ if (source[index] !== ":") return {
1110
+ explode: false,
1111
+ index,
1112
+ used: false
1113
+ };
1114
+ const digitsStart = index + 1;
1115
+ let digitsEnd = digitsStart;
1116
+ while (digitsEnd < source.length && isDigit(source[digitsEnd])) digitsEnd++;
1117
+ const prefix = source.slice(digitsStart, digitsEnd);
1118
+ if (!/^[1-9][0-9]{0,3}$/.test(prefix)) throw new InvalidPrefixError(template, offset + index, varSpec, prefix);
1119
+ return {
1120
+ explode: false,
1121
+ index: digitsEnd,
1122
+ prefix: Number(prefix),
1123
+ used: true
1124
+ };
1125
+ }
1126
+ function isOperator(char) {
1127
+ return OPERATORS.includes(char) && char !== "";
1128
+ }
1129
+ function isReservedOperator(char) {
1130
+ return reservedOperators.includes(char);
1131
+ }
1132
+ function isDigit(char) {
1133
+ const code = char.charCodeAt(0);
1134
+ return code >= 48 && code <= 57;
1135
+ }
1136
+ //#endregion
1137
+ //#region src/template/token.ts
1138
+ /**
1139
+ * Splits a URI Template source string into literal and expression tokens.
1140
+ *
1141
+ * This module validates RFC 6570 literal syntax and delegates expression
1142
+ * parsing to the expression parser.
1143
+ */
1144
+ function tokenize(template, options) {
1145
+ const { report } = options;
1146
+ const tokens = [];
1147
+ const appendLiteral = (text) => {
1148
+ const previous = tokens.at(-1);
1149
+ if (previous?.kind === "literal") tokens[tokens.length - 1] = {
1150
+ kind: "literal",
1151
+ text: previous.text + text
1152
+ };
1153
+ else tokens.push({
1154
+ kind: "literal",
1155
+ text
1156
+ });
1157
+ };
1158
+ for (let index = 0; index < template.length;) {
1159
+ const char = template[index];
1160
+ if (char === "{") {
1161
+ const closeIndex = template.indexOf("}", index + 1);
1162
+ if (closeIndex < 0) {
1163
+ report(new UnclosedExpressionError(template, index));
1164
+ appendLiteral(template.slice(index));
1165
+ break;
1166
+ }
1167
+ const nestedIndex = template.indexOf("{", index + 1);
1168
+ if (nestedIndex >= 0 && nestedIndex < closeIndex) {
1169
+ report(new NestedOpeningBraceError(template, nestedIndex));
1170
+ appendLiteral(template.slice(index, closeIndex + 1));
1171
+ index = closeIndex + 1;
1172
+ continue;
1173
+ }
1174
+ const expression = template.slice(index + 1, closeIndex);
1175
+ try {
1176
+ tokens.push(parseExpression(expression, template, index));
1177
+ } catch (error) {
1178
+ report(error instanceof Error ? error : new Error(String(error)));
1179
+ appendLiteral(template.slice(index, closeIndex + 1));
1180
+ }
1181
+ index = closeIndex + 1;
1182
+ continue;
1183
+ }
1184
+ if (char === "}") {
1185
+ report(new StrayClosingBraceError(template, index));
1186
+ appendLiteral(char);
1187
+ index++;
1188
+ continue;
1189
+ }
1190
+ const literalLength = isLiteralAt(template, index);
1191
+ if (literalLength > 0) {
1192
+ appendLiteral(template.slice(index, index + literalLength));
1193
+ index += literalLength;
1194
+ continue;
1195
+ }
1196
+ const { char: invalidChar, size } = readCodePoint(template, index);
1197
+ report(new InvalidLiteralError(template, index, invalidChar));
1198
+ appendLiteral(invalidChar);
1199
+ index += size;
1200
+ }
1201
+ return tokens;
1202
+ }
1203
+ //#endregion
1204
+ //#region src/template/template.ts
1205
+ /**
1206
+ * Parsed RFC 6570 URI Template that can be expanded repeatedly.
1207
+ *
1208
+ * This class owns tokenization and delegates expression expansion to the
1209
+ * expansion module. Instances are immutable after construction.
1210
+ */
1211
+ var Template = class Template {
1212
+ uriTemplate;
1213
+ options;
1214
+ #tokens;
1215
+ #fullOptions;
1216
+ constructor(uriTemplate, options = {}) {
1217
+ this.uriTemplate = uriTemplate;
1218
+ this.options = options;
1219
+ this.#fullOptions = fillOptions(options);
1220
+ this.#tokens = freezeTokens(tokenize(uriTemplate, this.#fullOptions));
1221
+ Object.freeze(this);
1222
+ }
1223
+ /**
1224
+ * Parses a URI Template using default strict parsing options.
1225
+ */
1226
+ static parse(uriTemplate, options = {}) {
1227
+ return new Template(uriTemplate, options);
1228
+ }
1229
+ /**
1230
+ * Immutable parsed token stream for diagnostics and router integration.
1231
+ */
1232
+ get tokens() {
1233
+ return this.#tokens;
1234
+ }
1235
+ /**
1236
+ * Expands this template against a variable context.
1237
+ */
1238
+ expand = (context) => expand(this.#tokens, context, this.#fullOptions);
1239
+ /**
1240
+ * Matches a URI against this template, returning the variable context if the
1241
+ * URI matches or `null` if it does not.
1242
+ */
1243
+ match = (uri) => match(this.#tokens, uri, this.#fullOptions);
1244
+ toString = () => this.uriTemplate;
1245
+ };
1246
+ const freezeTokens = (tokens) => Object.freeze(tokens.map(freezeToken));
1247
+ const freezeToken = (token) => token.kind === "literal" ? Object.freeze({
1248
+ kind: "literal",
1249
+ text: token.text
1250
+ }) : Object.freeze({
1251
+ kind: "expression",
1252
+ operator: token.operator,
1253
+ vars: Object.freeze(token.vars.map(freezeVarSpec))
1254
+ });
1255
+ const freezeVarSpec = (varSpec) => Object.freeze({
1256
+ name: varSpec.name,
1257
+ explode: varSpec.explode,
1258
+ ...varSpec.prefix == null ? {} : { prefix: varSpec.prefix }
1259
+ });
1260
+ const defaultReporter = () => void 0;
1261
+ const fillOptions = ({ strict, report }) => {
1262
+ report ??= defaultReporter;
1263
+ strict ??= true;
1264
+ report = strict ? strictWrapper(report) : report;
1265
+ return {
1266
+ strict,
1267
+ report
1268
+ };
1269
+ };
1270
+ const strictWrapper = (reporter) => (error) => {
1271
+ reporter(error);
1272
+ throw error;
1273
+ };
1274
+ //#endregion
1275
+ //#region src/utils.ts
1276
+ const isExpression = (token) => token.kind === "expression";
1277
+ const isLiteral = (token) => token.kind === "literal";
1278
+ /**
1279
+ * Reduces `iter` to a single value by applying `step` left-to-right, starting
1280
+ * from `init`.
1281
+ */
1282
+ const fold = (step, init, iter) => {
1283
+ let acc = init;
1284
+ for (const item of iter) acc = step(acc, item);
1285
+ return acc;
1286
+ };
1287
+ /**
1288
+ * Variant of {@link fold} that stops as soon as `step` yields `null` or
1289
+ * `undefined`, returning the last accumulator value that was defined. Useful
1290
+ * for descending through a trie where missing children fall back to the
1291
+ * deepest reachable node.
1292
+ */
1293
+ const foldWhileDefined = (step, init, iter) => {
1294
+ let acc = init;
1295
+ for (const item of iter) {
1296
+ const next = step(acc, item);
1297
+ if (next == null) break;
1298
+ acc = next;
1299
+ }
1300
+ return acc;
1301
+ };
1302
+ /**
1303
+ * Returns whether `path` is a path-shaped URI Template accepted by the
1304
+ * router.
1305
+ *
1306
+ * A path is either an empty string, a literal string starting with `/`, or a
1307
+ * path-expansion expression (`{/var}`).
1308
+ * Templates that fail to parse — and therefore could never be routed —
1309
+ * return `false`.
1310
+ */
1311
+ function isPath(path) {
1312
+ if (path === "") return true;
1313
+ try {
1314
+ const [first] = new Template(path).tokens;
1315
+ if (first == null) return false;
1316
+ if (isLiteral(first)) return first.text.startsWith("/");
1317
+ if (first.operator === "/") return true;
1318
+ return false;
1319
+ } catch {
1320
+ return false;
1321
+ }
1322
+ }
1323
+ function assertPath(path) {
1324
+ if (!isPath(path)) throw new RouterError(`"${path}" is not a router path. It must be empty, start with \`/\`, or start with a expression with slash(\`/\`) like \`{/id}\`.`);
1325
+ }
1326
+ //#endregion
1327
+ //#region src/router/fill.ts
1328
+ /**
1329
+ * Resolves a partial options input against a path pattern into fully-resolved
1330
+ * {@link RouteOptions}. Mirrors `fillOptions` in *template.ts*: missing
1331
+ * fields are filled with their defaults, with `multiple` derived from the
1332
+ * variable specification when not given. Throws when the supplied
1333
+ * `variables` keys do not match the template (under `exact`), when the same
1334
+ * variable name carries contradictory explode/prefix modifiers, or when a
1335
+ * per-field constraint is violated.
1336
+ */
1337
+ function fillRouteOptions(options = {}, pattern) {
1338
+ const overrides = options?.variables ?? {};
1339
+ const exact = options?.exact ?? true;
1340
+ if (exact && options?.variables != null) {
1341
+ const unknown = Object.keys(overrides).filter((key) => !pattern.variables.has(key));
1342
+ const missing = [...pattern.variables].filter((name) => !(name in overrides));
1343
+ const mismatched = [...unknown, ...missing];
1344
+ if (mismatched.length > 0) throw new RouteTemplateOptionsNotMatchedError(pattern.path, mismatched);
1345
+ }
1346
+ const operatorsByName = groupVarOperators(pattern.template.tokens);
1347
+ const variables = {};
1348
+ for (const [name, specs] of groupVarSpecs(pattern.template.tokens)) {
1349
+ const override = overrides[name];
1350
+ const hasExplode = specs.some((spec) => spec.explode);
1351
+ const hasPrefix = specs.some((spec) => spec.prefix != null);
1352
+ const hasPlain = specs.some((spec) => !spec.explode && spec.prefix == null);
1353
+ const multiple = override?.multiple;
1354
+ if (hasExplode && (hasPrefix || hasPlain) || hasExplode && multiple === false || hasPrefix && multiple === true) throw new ConflictingVarSpecError(pattern.path, name);
1355
+ variables[name] = {
1356
+ nullable: fillNullable(override?.nullable),
1357
+ multiple: fillMultiple(multiple, hasExplode, hasPrefix),
1358
+ duplicable: fillDuplicable(override?.duplicable, pattern.path, name, specs.length),
1359
+ prefixable: fillPrefixable(override?.prefixable, pattern.path, name, hasPrefix),
1360
+ explodable: fillExplodable(override?.explodable, pattern.path, name, hasExplode),
1361
+ operatables: fillOperatables(override?.operatables, pattern.path, name, operatorsByName.get(name))
1362
+ };
1363
+ }
1364
+ return {
1365
+ variables,
1366
+ exact
1367
+ };
1368
+ }
1369
+ const fillNullable = (override) => override ?? false;
1370
+ const fillMultiple = (requested, hasExplode, hasPrefix) => {
1371
+ if (hasExplode) return true;
1372
+ if (hasPrefix) return false;
1373
+ return requested ?? false;
1374
+ };
1375
+ const fillDuplicable = (override, template, name, occurrences) => {
1376
+ const duplicable = override ?? false;
1377
+ if (occurrences > 1 && !duplicable) throw new DuplicateRouteVariableError(template, name);
1378
+ return duplicable;
1379
+ };
1380
+ const fillPrefixable = (override, template, name, hasPrefix) => {
1381
+ const prefixable = override ?? false;
1382
+ if (hasPrefix && !prefixable) throw new DisallowedVarSpecModifierError(template, name, "prefix");
1383
+ return prefixable;
1384
+ };
1385
+ const fillExplodable = (override, template, name, hasExplode) => {
1386
+ const explodable = override ?? false;
1387
+ if (hasExplode && !explodable) throw new DisallowedVarSpecModifierError(template, name, "explode");
1388
+ return explodable;
1389
+ };
1390
+ const fillOperatables = (override, template, name, operators) => {
1391
+ const operatables = override ?? [];
1392
+ if (operatables.length > 0 && operators != null) {
1393
+ for (const operator of operators) if (!operatables.includes(operator)) throw new DisallowedOperatorError(template, name, operator);
1394
+ }
1395
+ return operatables;
1396
+ };
1397
+ const groupVarSpecs = (tokens) => {
1398
+ const grouped = /* @__PURE__ */ new Map();
1399
+ for (const token of tokens) {
1400
+ if (!isExpression(token)) continue;
1401
+ for (const varSpec of token.vars) {
1402
+ const list = grouped.get(varSpec.name);
1403
+ if (list == null) grouped.set(varSpec.name, [varSpec]);
1404
+ else list.push(varSpec);
1405
+ }
1406
+ }
1407
+ return grouped;
1408
+ };
1409
+ const groupVarOperators = (tokens) => {
1410
+ const grouped = /* @__PURE__ */ new Map();
1411
+ for (const token of tokens) {
1412
+ if (!isExpression(token)) continue;
1413
+ for (const varSpec of token.vars) {
1414
+ const set = grouped.get(varSpec.name);
1415
+ if (set == null) grouped.set(varSpec.name, new Set([token.operator]));
1416
+ else set.add(token.operator);
1417
+ }
1418
+ }
1419
+ return grouped;
1420
+ };
1421
+ //#endregion
1422
+ //#region src/router/trie/priority.ts
1423
+ const compareRouteEntries = (left, right) => right.literalLength - left.literalLength || right.initialLiteralPrefix.length - left.initialLiteralPrefix.length || left.variableCount - right.variableCount || left.index - right.index;
1424
+ const mergeRouteEntries = (left, right) => {
1425
+ if (left.length < 1) return right;
1426
+ if (right.length < 1) return left;
1427
+ const merged = [];
1428
+ let leftIndex = 0;
1429
+ let rightIndex = 0;
1430
+ while (leftIndex < left.length && rightIndex < right.length) if (compareRouteEntries(left[leftIndex], right[rightIndex]) <= 0) merged.push(left[leftIndex++]);
1431
+ else merged.push(right[rightIndex++]);
1432
+ while (leftIndex < left.length) merged.push(left[leftIndex++]);
1433
+ while (rightIndex < right.length) merged.push(right[rightIndex++]);
1434
+ return merged;
1435
+ };
1436
+ //#endregion
1437
+ //#region src/router/trie/node.ts
1438
+ /**
1439
+ * Base trie node storing entries sorted by route priority.
1440
+ */
1441
+ var Node = class {
1442
+ #entries = [];
1443
+ get entries() {
1444
+ return this.#entries;
1445
+ }
1446
+ insert = (entry) => {
1447
+ this.#entries.splice(this.#insertionIndex(entry), 0, entry);
1448
+ };
1449
+ remove = (entry) => {
1450
+ const index = this.#entries.indexOf(entry);
1451
+ if (index < 0) return;
1452
+ this.#entries.splice(index, 1);
1453
+ };
1454
+ #insertionIndex(entry) {
1455
+ let low = 0;
1456
+ let high = this.#entries.length;
1457
+ while (low < high) {
1458
+ const middle = Math.floor((low + high) / 2);
1459
+ if (compareRouteEntries(this.#entries[middle], entry) <= 0) low = middle + 1;
1460
+ else high = middle;
1461
+ }
1462
+ return low;
1463
+ }
1464
+ };
1465
+ //#endregion
1466
+ //#region src/router/trie/fallback/node.ts
1467
+ /**
1468
+ * Prefix-trie node for routes the main router index cannot handle.
1469
+ * Precomputes merged candidates per node so path lookups only read the
1470
+ * deepest matching node's {@link candidates}.
1471
+ */
1472
+ var FallbackNode = class FallbackNode extends Node {
1473
+ #children = /* @__PURE__ */ new Map();
1474
+ #candidates = [];
1475
+ get candidates() {
1476
+ return this.#candidates;
1477
+ }
1478
+ child = (key) => this.#children.get(key);
1479
+ childOrInsert = (key) => {
1480
+ const existing = this.#children.get(key);
1481
+ if (existing != null) return existing;
1482
+ const inserted = new FallbackNode();
1483
+ this.#children.set(key, inserted);
1484
+ return inserted;
1485
+ };
1486
+ rebuildCandidates = (parentCandidates) => {
1487
+ this.#candidates = mergeRouteEntries(parentCandidates, this.entries);
1488
+ for (const child of this.#children.values()) child.rebuildCandidates(this.#candidates);
1489
+ };
1490
+ };
1491
+ //#endregion
1492
+ //#region src/router/trie/fallback/trie.ts
1493
+ /**
1494
+ * Prefix trie for route templates that cannot be safely compiled into the
1495
+ * token-level state trie.
1496
+ */
1497
+ var FallbackTrie = class {
1498
+ #root = new FallbackNode();
1499
+ #dirty = true;
1500
+ insert = (entry) => {
1501
+ fold((n, char) => n.childOrInsert(char), this.#root, entry.initialLiteralPrefix).insert(entry);
1502
+ this.#dirty = true;
1503
+ };
1504
+ remove = (entry) => {
1505
+ foldWhileDefined((node, char) => node.child(char), this.#root, entry.initialLiteralPrefix).remove(entry);
1506
+ this.#dirty = true;
1507
+ };
1508
+ *candidates(path) {
1509
+ if (this.#dirty) this.#rebuildCandidates();
1510
+ yield* this.#deepestNode(path).candidates;
1511
+ }
1512
+ #deepestNode = (path) => foldWhileDefined((n, char) => n.child(char), this.#root, path);
1513
+ #rebuildCandidates = () => {
1514
+ this.#root.rebuildCandidates([]);
1515
+ this.#dirty = false;
1516
+ };
1517
+ };
1518
+ //#endregion
1519
+ //#region src/router/trie/state/node.ts
1520
+ var StateNode = class StateNode extends Node {
1521
+ literalEdges = [];
1522
+ expressionEdges = /* @__PURE__ */ new Map();
1523
+ insertLiteral = (text) => {
1524
+ if (text === "") return this;
1525
+ const edge = this.#findCommonLiteralEdge(text);
1526
+ if (edge == null) return this.#appendLiteralEdge(text);
1527
+ const length = commonPrefixLength(edge.text, text);
1528
+ if (length === edge.text.length) return edge.node.insertLiteral(text.slice(length));
1529
+ const intermediate = splitLiteralEdge(edge, length);
1530
+ if (length === text.length) return intermediate;
1531
+ return intermediate.#appendLiteralEdge(text.slice(length));
1532
+ };
1533
+ findLiteral = (text) => {
1534
+ if (text === "") return this;
1535
+ const edge = this.literalEdges.find(({ text: edgeText }) => text.startsWith(edgeText));
1536
+ if (edge == null) return void 0;
1537
+ return edge.node.findLiteral(text.slice(edge.text.length));
1538
+ };
1539
+ childOrInsertExpression = ({ key, operator }) => {
1540
+ const existing = this.expressionEdges.get(key);
1541
+ if (existing != null) return existing.node;
1542
+ const node = new StateNode();
1543
+ this.expressionEdges.set(key, {
1544
+ key,
1545
+ operator,
1546
+ node
1547
+ });
1548
+ return node;
1549
+ };
1550
+ childExpression = (key) => this.expressionEdges.get(key)?.node;
1551
+ #appendLiteralEdge(text) {
1552
+ const child = new StateNode();
1553
+ this.literalEdges.push({
1554
+ text,
1555
+ node: child
1556
+ });
1557
+ return child;
1558
+ }
1559
+ #findCommonLiteralEdge = (text) => this.literalEdges.find((edge) => commonPrefixLength(edge.text, text) > 0);
1560
+ };
1561
+ const splitLiteralEdge = (edge, length) => {
1562
+ const intermediate = new StateNode();
1563
+ const suffix = edge.text.slice(length);
1564
+ intermediate.literalEdges.push({
1565
+ text: suffix,
1566
+ node: edge.node
1567
+ });
1568
+ edge.text = edge.text.slice(0, length);
1569
+ edge.node = intermediate;
1570
+ return intermediate;
1571
+ };
1572
+ const commonPrefixLength = (left, right) => {
1573
+ const max = Math.min(left.length, right.length);
1574
+ for (let index = 0; index < max; index++) if (left[index] !== right[index]) return index;
1575
+ return max;
1576
+ };
1577
+ //#endregion
1578
+ //#region src/router/trie/state/trie.ts
1579
+ /**
1580
+ * Token-level state trie for common path-shaped URI templates.
1581
+ */
1582
+ var StateTrie = class {
1583
+ #root = new StateNode();
1584
+ insert = (entry) => fold((node, segment) => isLiteral(segment) ? node.insertLiteral(segment.text) : node.childOrInsertExpression(segment), this.#root, compileSegments(entry.tokens)).insert(entry);
1585
+ remove = (entry) => foldWhileDefined((node, segment) => isLiteral(segment) ? node.findLiteral(segment.text) : node.childExpression(segment.key), this.#root, compileSegments(entry.tokens)).remove(entry);
1586
+ *candidates(path) {
1587
+ yield* stateEntries(this.#root, path, 0);
1588
+ }
1589
+ };
1590
+ function* compileSegments(tokens) {
1591
+ for (const token of tokens) if (isLiteral(token)) {
1592
+ if (token.text !== "") yield token;
1593
+ } else yield {
1594
+ kind: "expression",
1595
+ key: expressionKey(token),
1596
+ operator: token.operator
1597
+ };
1598
+ }
1599
+ const expressionKey = ({ operator, vars: [{ explode, prefix }] }) => [
1600
+ operator,
1601
+ explode ? "*" : "",
1602
+ prefix ?? ""
1603
+ ].join("\0");
1604
+ function* stateEntries(node, path, index) {
1605
+ if (index === path.length) yield* node.entries;
1606
+ for (const edge of node.literalEdges) if (path.startsWith(edge.text, index)) yield* stateEntries(edge.node, path, index + edge.text.length);
1607
+ for (const edge of node.expressionEdges.values()) yield* expressionEntries(edge, path, index);
1608
+ }
1609
+ function* expressionEntries(edge, path, index) {
1610
+ for (const end of expressionEndIndexes(edge.node, path, index)) {
1611
+ const expression = path.slice(index, end);
1612
+ if (!matchesExpressionShape(edge.operator, expression)) continue;
1613
+ yield* stateEntries(edge.node, path, end);
1614
+ }
1615
+ }
1616
+ function* expressionEndIndexes(node, path, index) {
1617
+ if (node.entries.length > 0) yield path.length;
1618
+ for (const edge of node.literalEdges) for (let found = path.indexOf(edge.text, index); found >= 0; found = path.indexOf(edge.text, found + 1)) yield found;
1619
+ }
1620
+ const matchesExpressionShape = (operator, expression) => {
1621
+ if (expression === "") return true;
1622
+ if (operator === "") return !expression.includes("/");
1623
+ if (operator === "/") return expression.startsWith("/");
1624
+ return true;
1625
+ };
1626
+ //#endregion
1627
+ //#region src/router/trie/mod.ts
1628
+ /**
1629
+ * Route candidate trie that delegates to a state trie for indexable templates
1630
+ * and a fallback prefix trie for the remaining RFC 6570 shapes.
1631
+ */
1632
+ var Trie = class {
1633
+ #state = new StateTrie();
1634
+ #fallback = new FallbackTrie();
1635
+ insert = (entry) => {
1636
+ if (isStateTrieEntry(entry)) this.#state.insert(entry);
1637
+ else this.#fallback.insert(entry);
1638
+ };
1639
+ remove = (entry) => {
1640
+ if (isStateTrieEntry(entry)) this.#state.remove(entry);
1641
+ else this.#fallback.remove(entry);
1642
+ };
1643
+ insertAll = (entries) => {
1644
+ for (const entry of entries) this.insert(entry);
1645
+ };
1646
+ *candidates(path) {
1647
+ yield* Array.from(uniqueMergedEntries(this.#state.candidates(path), this.#fallback.candidates(path))).sort(compareRouteEntries);
1648
+ }
1649
+ };
1650
+ const isStateTrieEntry = (entry) => isIndexableRoute(entry.tokens);
1651
+ const isIndexableRoute = (tokens) => {
1652
+ let previousWasExpression = false;
1653
+ for (const token of tokens) if (isLiteral(token)) previousWasExpression = false;
1654
+ else {
1655
+ if (previousWasExpression) return false;
1656
+ if (!isIndexableExpression(token)) return false;
1657
+ previousWasExpression = true;
1658
+ }
1659
+ return true;
1660
+ };
1661
+ const isIndexableExpression = ({ vars, operator }) => vars.length === 1 && INDEXABLE_OPERATORS.has(operator);
1662
+ const INDEXABLE_OPERATORS = new Set([
1663
+ "",
1664
+ "/",
1665
+ "+"
1666
+ ]);
1667
+ function uniqueMergedEntries(...sources) {
1668
+ const seen = /* @__PURE__ */ new Set();
1669
+ for (const source of sources) for (const entry of source) seen.add(entry);
1670
+ return seen;
1671
+ }
1672
+ //#endregion
1673
+ //#region src/router/router.ts
1674
+ /**
1675
+ * Router that resolves URIs against registered RFC 6570 templates.
1676
+ */
1677
+ var Router = class Router {
1678
+ #trie;
1679
+ #routesByName;
1680
+ #prevIndex = -1;
1681
+ /**
1682
+ * Whether to ignore trailing slashes when matching paths.
1683
+ */
1684
+ trailingSlashInsensitive;
1685
+ constructor(routesOrOptions, maybeOptions) {
1686
+ const routes = isRoutesArgument(routesOrOptions) ? routesOrOptions : void 0;
1687
+ const options = isRoutesArgument(routesOrOptions) ? maybeOptions : routesOrOptions;
1688
+ this.#trie = new Trie();
1689
+ this.#routesByName = /* @__PURE__ */ new Map();
1690
+ this.trailingSlashInsensitive = options?.trailingSlashInsensitive ?? false;
1691
+ if (routes != null) this.register(routes);
1692
+ }
1693
+ static from(routesOrOptions, options) {
1694
+ return new Router(routesOrOptions, options);
1695
+ }
1696
+ /**
1697
+ * Compiles a path template without registering it in a router.
1698
+ * @param path The path pattern.
1699
+ * @returns A parsed path pattern.
1700
+ */
1701
+ static compile(path) {
1702
+ if (!isPath(path)) throw new RouteTemplatePathError(path);
1703
+ const template = Template.parse(path);
1704
+ return Object.freeze({
1705
+ path,
1706
+ template,
1707
+ variables: collectVariables(template.tokens)
1708
+ });
1709
+ }
1710
+ /**
1711
+ * Returns the variable names in a path template without registering it.
1712
+ * @param path The path pattern.
1713
+ * @returns The names of the variables in the path pattern.
1714
+ */
1715
+ static variables = (path) => new Set(Router.compile(path).variables);
1716
+ /**
1717
+ * Checks if a path name exists in the router.
1718
+ * @param name The name of the path.
1719
+ * @returns `true` if the path name exists, otherwise `false`.
1720
+ */
1721
+ has = (name) => this.#routesByName.has(name);
1722
+ /**
1723
+ * Adds a new path rule to the router.
1724
+ * @param pathOrPattern The path template, or a pre-parsed
1725
+ * {@link RouterPathPattern} produced by
1726
+ * {@link Router.compile}.
1727
+ * @param name The name of the path.
1728
+ * @param options Per-route options, including per-variable constraints.
1729
+ */
1730
+ add = (pathOrPattern, name, options) => {
1731
+ const pattern = resolvePathPattern(pathOrPattern);
1732
+ const previous = this.#routesByName.get(name);
1733
+ if (previous != null) this.#trie.remove(previous);
1734
+ const entry = createRouteEntry({
1735
+ index: this.#index,
1736
+ name,
1737
+ pattern,
1738
+ options: fillRouteOptions(options, pattern)
1739
+ });
1740
+ this.#routesByName.set(name, entry);
1741
+ this.#trie.insert(entry);
1742
+ };
1743
+ /**
1744
+ * Registers multiple path rules at once.
1745
+ * @param routes Iterable of `[pathOrPattern, name]` pairs to register.
1746
+ */
1747
+ register = (routes) => {
1748
+ const resolved = Iterator.from(routes).map(([pathOrPattern, name, options]) => [
1749
+ resolvePathPattern(pathOrPattern),
1750
+ name,
1751
+ options
1752
+ ]).map(([pattern, name, options]) => [name, createRouteEntry({
1753
+ index: this.#index,
1754
+ name,
1755
+ pattern,
1756
+ options: fillRouteOptions(options, pattern)
1757
+ })]);
1758
+ const pending = new Map(resolved);
1759
+ for (const name of pending.keys()) {
1760
+ const committed = this.#routesByName.get(name);
1761
+ if (committed != null) this.#trie.remove(committed);
1762
+ }
1763
+ for (const [name, entry] of pending) this.#routesByName.set(name, entry);
1764
+ this.#trie.insertAll(pending.values());
1765
+ };
1766
+ get #index() {
1767
+ return this.#prevIndex++;
1768
+ }
1769
+ /**
1770
+ * Resolves a path name and values from a URI, if any match.
1771
+ * @param url The URI to resolve.
1772
+ * @returns The name of the path and its values, if any match. Otherwise,
1773
+ * `null`.
1774
+ */
1775
+ route = (url) => this.#route(url) ?? (this.trailingSlashInsensitive ? this.#route(toggleTrailingSlash(url)) : null);
1776
+ #route(url) {
1777
+ for (const entry of this.#trie.candidates(url)) {
1778
+ const context = entry.pattern.template.match(url);
1779
+ if (context == null) continue;
1780
+ const values = resolveValues(context, entry);
1781
+ if (values == null) continue;
1782
+ return {
1783
+ name: entry.name,
1784
+ template: entry.pattern.path,
1785
+ values
1786
+ };
1787
+ }
1788
+ return null;
1789
+ }
1790
+ /**
1791
+ * Constructs a URL/path from a path name and values.
1792
+ * @param name The name of the path.
1793
+ * @param values The values to expand the path with.
1794
+ * @returns The URL/path, if the name exists. Otherwise, `null`.
1795
+ */
1796
+ build = (name, values) => this.#routesByName.get(name)?.pattern.template.expand(values) ?? null;
1797
+ /**
1798
+ * Creates a shallow clone of the router. The clone shares immutable
1799
+ * registered path patterns with the original, but changes to the route set
1800
+ * (adding, removing, or re-registering routes) do not affect the other
1801
+ * router.
1802
+ * @returns A new router with the same routes and options as this one.
1803
+ */
1804
+ clone = () => new Router(this.#activeEntries(), { trailingSlashInsensitive: this.trailingSlashInsensitive });
1805
+ #activeEntries = () => Array.from(this.#routesByName.values()).sort((left, right) => left.index - right.index).map((entry) => [
1806
+ entry.pattern,
1807
+ entry.name,
1808
+ entry.options
1809
+ ]);
1810
+ };
1811
+ const createRouteEntry = ({ index, name, pattern, options }) => ({
1812
+ index,
1813
+ name,
1814
+ pattern,
1815
+ tokens: pattern.template.tokens,
1816
+ initialLiteralPrefix: getInitialLiteralPrefix(pattern.template.tokens),
1817
+ literalLength: getLiteralLength(pattern.template.tokens),
1818
+ variableCount: pattern.variables.size,
1819
+ options,
1820
+ constraints: new Map(Object.entries(options.variables))
1821
+ });
1822
+ const resolvePathPattern = (value) => typeof value === "string" ? Router.compile(value) : value;
1823
+ const isRoutesArgument = (value) => value != null && typeof value === "object" && Symbol.iterator in value;
1824
+ const toggleTrailingSlash = (path) => path.endsWith("/") ? path.replace(/\/+$/, "") : `${path}/`;
1825
+ const collectVariables = (tokens) => new ImmutableSet(tokens.filter(isExpression).flatMap((token) => token.vars.map((varSpec) => varSpec.name)));
1826
+ var ImmutableSet = class extends Set {
1827
+ constructor(values) {
1828
+ super();
1829
+ if (values != null) for (const value of values) super.add(value);
1830
+ }
1831
+ add(_value) {
1832
+ throw new TypeError("ImmutableSet cannot be mutated.");
1833
+ }
1834
+ delete(_value) {
1835
+ throw new TypeError("ImmutableSet cannot be mutated.");
1836
+ }
1837
+ clear() {
1838
+ throw new TypeError("ImmutableSet cannot be mutated.");
1839
+ }
1840
+ };
1841
+ const getInitialLiteralPrefix = (tokens) => tokens[0] != null && isLiteral(tokens[0]) ? tokens[0].text : "";
1842
+ const getLiteralLength = (tokens) => tokens.reduce((sum, token) => isLiteral(token) ? sum + token.text.length : sum, 0);
1843
+ /**
1844
+ * Applies the resolved per-variable constraints to a matched context,
1845
+ * producing the route values or `null` when a constraint rejects the match
1846
+ * (so the caller falls back to the next candidate).
1847
+ */
1848
+ const resolveValues = (context, entry) => {
1849
+ const values = {};
1850
+ for (const [name, constraint] of entry.constraints) {
1851
+ const raw = context[name];
1852
+ if (constraint.multiple) {
1853
+ const list = toStringList(raw);
1854
+ if (list == null) return null;
1855
+ if ((list.length < 1 || list.every((item) => item === "")) && !constraint.nullable) return null;
1856
+ values[name] = list;
1857
+ continue;
1858
+ }
1859
+ if (typeof raw === "string") {
1860
+ if (raw === "" && !constraint.nullable) return null;
1861
+ values[name] = raw;
1862
+ } else if (!constraint.nullable) return null;
1863
+ else values[name] = null;
1864
+ }
1865
+ return values;
1866
+ };
1867
+ const toStringList = (value) => {
1868
+ if (value === void 0) return [];
1869
+ if (typeof value === "string") return [value];
1870
+ if (Array.isArray(value)) return value.every((item) => typeof item === "string") ? value : null;
1871
+ return null;
1872
+ };
1873
+ //#endregion
1874
+ exports.ConflictingVarSpecError = ConflictingVarSpecError;
1875
+ exports.DisallowedOperatorError = DisallowedOperatorError;
1876
+ exports.DisallowedVarSpecModifierError = DisallowedVarSpecModifierError;
1877
+ exports.DuplicateRouteVariableError = DuplicateRouteVariableError;
1878
+ exports.EmptyExpressionError = EmptyExpressionError;
1879
+ exports.EmptyVarNameError = EmptyVarNameError;
1880
+ exports.InvalidLiteralError = InvalidLiteralError;
1881
+ exports.InvalidPrefixError = InvalidPrefixError;
1882
+ exports.InvalidVarNameError = InvalidVarNameError;
1883
+ exports.InvalidVarSpecError = InvalidVarSpecError;
1884
+ exports.NestedOpeningBraceError = NestedOpeningBraceError;
1885
+ exports.PrefixModifierNotApplicableError = PrefixModifierNotApplicableError;
1886
+ exports.ReservedOperatorError = ReservedOperatorError;
1887
+ exports.RouteTemplateOptionsNotMatchedError = RouteTemplateOptionsNotMatchedError;
1888
+ exports.RouteTemplatePathError = RouteTemplatePathError;
1889
+ exports.Router = Router;
1890
+ exports.RouterError = RouterError;
1891
+ exports.StrayClosingBraceError = StrayClosingBraceError;
1892
+ exports.Template = Template;
1893
+ exports.TemplateExpansionError = TemplateExpansionError;
1894
+ exports.TemplateParseError = TemplateParseError;
1895
+ exports.TrailingCommaError = TrailingCommaError;
1896
+ exports.UnclosedExpressionError = UnclosedExpressionError;
1897
+ exports.UnexpectedCharacterError = UnexpectedCharacterError;
1898
+ exports.UnknownOperatorError = UnknownOperatorError;
1899
+ exports.assertPath = assertPath;
1900
+ exports.isExpression = isExpression;
1901
+ exports.isPath = isPath;