@ccheever/exact-ibex-runtime 0.1.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/package.json +63 -0
- package/src/abort/AbortController.ts +23 -0
- package/src/abort/AbortSignal.ts +152 -0
- package/src/abort/index.ts +2 -0
- package/src/accessibility.ts +12 -0
- package/src/arraybuffer-detach.ts +109 -0
- package/src/base64/base64.ts +168 -0
- package/src/base64/index.ts +1 -0
- package/src/blob/Blob.ts +259 -0
- package/src/blob/File.ts +59 -0
- package/src/blob/FormData.ts +323 -0
- package/src/blob/index.ts +3 -0
- package/src/bootstrap.ts +1946 -0
- package/src/broadcast/BroadcastChannel.ts +280 -0
- package/src/broadcast/index.ts +5 -0
- package/src/cache/Cache.ts +349 -0
- package/src/cache/CacheStorage.ts +89 -0
- package/src/cache/index.ts +27 -0
- package/src/camera/index.ts +6202 -0
- package/src/camera/processor.worker.ts +194 -0
- package/src/camera/scene.ts +195 -0
- package/src/clipboard/Clipboard.ts +129 -0
- package/src/clipboard/ClipboardItem.ts +97 -0
- package/src/clipboard/index.ts +6 -0
- package/src/clone/index.ts +1 -0
- package/src/clone/structuredClone.ts +389 -0
- package/src/clone/transferableSymbols.ts +2 -0
- package/src/compression/CompressionStream.ts +146 -0
- package/src/compression/DecompressionStream.ts +342 -0
- package/src/compression/index.ts +4 -0
- package/src/console/Console.ts +341 -0
- package/src/console/index.ts +2 -0
- package/src/core/accessibility-state.ts +263 -0
- package/src/core/accessibility.ts +184 -0
- package/src/core/agent-state.ts +37 -0
- package/src/core/diagnostics-logs.ts +144 -0
- package/src/core/host-call-bridge.ts +16 -0
- package/src/core/i18n-helpers.ts +189 -0
- package/src/core/locale-state.ts +253 -0
- package/src/core/locale.ts +95 -0
- package/src/crypto/Crypto.ts +2743 -0
- package/src/crypto/index.ts +1 -0
- package/src/diagnostics/logs.ts +7 -0
- package/src/encoding/TextDecoder.ts +1181 -0
- package/src/encoding/TextDecoderStream.ts +58 -0
- package/src/encoding/TextEncoder.ts +180 -0
- package/src/encoding/TextEncoderStream.ts +39 -0
- package/src/encoding/index.ts +8 -0
- package/src/events/CloseEvent.ts +91 -0
- package/src/events/DOMException.ts +409 -0
- package/src/events/ErrorEvent.ts +39 -0
- package/src/events/Event.ts +151 -0
- package/src/events/EventTarget.ts +280 -0
- package/src/events/FocusEvent.ts +27 -0
- package/src/events/KeyboardEvent.ts +46 -0
- package/src/events/MessageEvent.ts +61 -0
- package/src/events/ProgressEvent.ts +33 -0
- package/src/events/PromiseRejectionEvent.ts +31 -0
- package/src/events/index.ts +52 -0
- package/src/eventsource/EventSource.ts +371 -0
- package/src/eventsource/index.ts +2 -0
- package/src/fetch/Headers.ts +642 -0
- package/src/fetch/Request.ts +760 -0
- package/src/fetch/Response.ts +543 -0
- package/src/fetch/body.ts +1256 -0
- package/src/fetch/cookie-jar.ts +566 -0
- package/src/fetch/demo.ts +207 -0
- package/src/fetch/errors.ts +101 -0
- package/src/fetch/fetch.ts +2610 -0
- package/src/fetch/index.ts +101 -0
- package/src/fetch/native-bridge.ts +65 -0
- package/src/fetch/types.ts +258 -0
- package/src/filereader/FileReader.ts +236 -0
- package/src/filereader/index.ts +1 -0
- package/src/fs/Dirent.ts +39 -0
- package/src/fs/ExactFile.ts +450 -0
- package/src/fs/Stats.ts +80 -0
- package/src/fs/index.ts +944 -0
- package/src/fs/promises.ts +386 -0
- package/src/fs/shared.ts +328 -0
- package/src/http-server/index.js +697 -0
- package/src/http-server/index.ts +27 -0
- package/src/identity.generated.ts +14 -0
- package/src/index.ts +283 -0
- package/src/indexeddb/IDBCursor.ts +188 -0
- package/src/indexeddb/IDBDatabase.ts +343 -0
- package/src/indexeddb/IDBFactory.ts +269 -0
- package/src/indexeddb/IDBIndex.ts +194 -0
- package/src/indexeddb/IDBKeyRange.ts +109 -0
- package/src/indexeddb/IDBObjectStore.ts +468 -0
- package/src/indexeddb/IDBRequest.ts +163 -0
- package/src/indexeddb/IDBTransaction.ts +207 -0
- package/src/indexeddb/index.ts +34 -0
- package/src/indexeddb/utils.ts +52 -0
- package/src/inspect/index.ts +1 -0
- package/src/inspect/inspect.ts +465 -0
- package/src/internal/detect.ts +104 -0
- package/src/locale.ts +10 -0
- package/src/location/index.ts +1059 -0
- package/src/locks/LockManager.ts +460 -0
- package/src/locks/index.ts +12 -0
- package/src/media/VideoFrame.ts +58 -0
- package/src/messaging/MessageChannel.ts +31 -0
- package/src/messaging/MessagePort.ts +180 -0
- package/src/messaging/index.ts +2 -0
- package/src/messaging.ts +247 -0
- package/src/native/NativeModules.ts +354 -0
- package/src/native/index.ts +1 -0
- package/src/navigator/Navigator.ts +351 -0
- package/src/navigator/index.ts +1 -0
- package/src/node/Buffer.ts +1786 -0
- package/src/node/index.ts +4 -0
- package/src/node/path.ts +495 -0
- package/src/node/process.ts +2528 -0
- package/src/performance/Performance.ts +532 -0
- package/src/performance/index.ts +21 -0
- package/src/polyfills/array.ts +236 -0
- package/src/polyfills/arraybuffer.ts +172 -0
- package/src/polyfills/groupby.ts +85 -0
- package/src/polyfills/index.ts +85 -0
- package/src/polyfills/intl.ts +1956 -0
- package/src/polyfills/iterator.ts +479 -0
- package/src/polyfills/promise.ts +37 -0
- package/src/polyfills/set.ts +245 -0
- package/src/polyfills/string.ts +85 -0
- package/src/polyfills/typedarray.ts +110 -0
- package/src/promise-rejection-tracking.ts +464 -0
- package/src/react-native/index.ts +388 -0
- package/src/runtime-entry.ts +55 -0
- package/src/scheduling/AnimationFrame.ts +105 -0
- package/src/scheduling/IdleCallback.ts +167 -0
- package/src/scheduling/index.ts +13 -0
- package/src/security/Capabilities.ts +1146 -0
- package/src/security/Permissions.ts +392 -0
- package/src/security/capability-bits.generated.ts +63 -0
- package/src/security/index.ts +16 -0
- package/src/sqlite/Database.ts +456 -0
- package/src/sqlite/Statement.ts +206 -0
- package/src/sqlite/constants.ts +79 -0
- package/src/sqlite/errors.ts +25 -0
- package/src/sqlite/index.ts +34 -0
- package/src/sqlite/module.js +438 -0
- package/src/storage/Storage.ts +291 -0
- package/src/storage/StorageManager.ts +91 -0
- package/src/storage/index.ts +3 -0
- package/src/stream-compat.ts +47 -0
- package/src/streams/ReadableStream.ts +4131 -0
- package/src/streams/TransformStream.ts +375 -0
- package/src/streams/WritableStream.ts +866 -0
- package/src/streams/index.ts +41 -0
- package/src/timers/Timers.ts +296 -0
- package/src/timers/index.ts +11 -0
- package/src/url/URL.ts +656 -0
- package/src/url/URLPattern.ts +850 -0
- package/src/url/URLSearchParams.ts +244 -0
- package/src/url/index.ts +9 -0
- package/src/websocket/WebSocket.ts +770 -0
- package/src/websocket/WebSocketError.ts +52 -0
- package/src/websocket/WebSocketStream.ts +628 -0
- package/src/websocket/index.ts +7 -0
- package/src/window/index.ts +872 -0
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URLPattern - Web API Implementation
|
|
3
|
+
*
|
|
4
|
+
* Implements the URLPattern standard for matching URLs against patterns.
|
|
5
|
+
* Supports wildcards (*), named groups (:name), regex groups ((regex)),
|
|
6
|
+
* and optional segments ({pattern}?).
|
|
7
|
+
*
|
|
8
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/URLPattern
|
|
9
|
+
* @see https://urlpattern.spec.whatwg.org/
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Input for URLPattern constructor - either a string URL pattern or
|
|
18
|
+
* an object with component patterns.
|
|
19
|
+
*/
|
|
20
|
+
export interface URLPatternInit {
|
|
21
|
+
protocol?: string;
|
|
22
|
+
username?: string;
|
|
23
|
+
password?: string;
|
|
24
|
+
hostname?: string;
|
|
25
|
+
port?: string;
|
|
26
|
+
pathname?: string;
|
|
27
|
+
search?: string;
|
|
28
|
+
hash?: string;
|
|
29
|
+
baseURL?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Result of a component match, containing the matched input and
|
|
34
|
+
* any named/positional capture groups.
|
|
35
|
+
*/
|
|
36
|
+
export interface URLPatternComponentResult {
|
|
37
|
+
input: string;
|
|
38
|
+
groups: Record<string, string | undefined>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Full result of URLPattern.exec(), with match results for each component.
|
|
43
|
+
*/
|
|
44
|
+
export interface URLPatternResult {
|
|
45
|
+
inputs: [URLPatternInput] | [URLPatternInput, string];
|
|
46
|
+
protocol: URLPatternComponentResult;
|
|
47
|
+
username: URLPatternComponentResult;
|
|
48
|
+
password: URLPatternComponentResult;
|
|
49
|
+
hostname: URLPatternComponentResult;
|
|
50
|
+
port: URLPatternComponentResult;
|
|
51
|
+
pathname: URLPatternComponentResult;
|
|
52
|
+
search: URLPatternComponentResult;
|
|
53
|
+
hash: URLPatternComponentResult;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type URLPatternInput = string | URLPatternInit;
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Internal: Pattern Compilation
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Represents a compiled component pattern with its regex and group names.
|
|
64
|
+
*/
|
|
65
|
+
interface CompiledComponent {
|
|
66
|
+
pattern: string;
|
|
67
|
+
regex: RegExp;
|
|
68
|
+
groupNames: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The set of URL component names we track. */
|
|
72
|
+
const COMPONENTS = [
|
|
73
|
+
"protocol",
|
|
74
|
+
"username",
|
|
75
|
+
"password",
|
|
76
|
+
"hostname",
|
|
77
|
+
"port",
|
|
78
|
+
"pathname",
|
|
79
|
+
"search",
|
|
80
|
+
"hash",
|
|
81
|
+
] as const;
|
|
82
|
+
|
|
83
|
+
type ComponentName = (typeof COMPONENTS)[number];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Default patterns for each component when not specified.
|
|
87
|
+
* These match "anything" in a way appropriate for that component.
|
|
88
|
+
*/
|
|
89
|
+
const DEFAULT_PATTERNS: Record<ComponentName, string> = {
|
|
90
|
+
protocol: "*",
|
|
91
|
+
username: "*",
|
|
92
|
+
password: "*",
|
|
93
|
+
hostname: "*",
|
|
94
|
+
port: "*",
|
|
95
|
+
pathname: "*",
|
|
96
|
+
search: "*",
|
|
97
|
+
hash: "*",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Characters that separate segments in each component.
|
|
102
|
+
* Used to determine what a bare `:name` group should NOT match.
|
|
103
|
+
*/
|
|
104
|
+
function getSegmentWildcard(component: ComponentName): string {
|
|
105
|
+
switch (component) {
|
|
106
|
+
case "pathname":
|
|
107
|
+
// Named groups in pathname should not cross `/`
|
|
108
|
+
return "[^/]+";
|
|
109
|
+
case "hostname":
|
|
110
|
+
// Named groups in hostname should not cross `.`
|
|
111
|
+
return "[^.]+";
|
|
112
|
+
default:
|
|
113
|
+
// For other components, match everything
|
|
114
|
+
return "[^]+";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* For a full wildcard `*`, determine what regex to use.
|
|
120
|
+
*/
|
|
121
|
+
function getFullWildcard(component: ComponentName): string {
|
|
122
|
+
return ".*";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Escape a string for use in a regular expression.
|
|
127
|
+
*/
|
|
128
|
+
function escapeRegExp(str: string): string {
|
|
129
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse and compile a pattern string for a given component into a RegExp.
|
|
134
|
+
*
|
|
135
|
+
* Supported syntax:
|
|
136
|
+
* :name - Named parameter (matches segment chars)
|
|
137
|
+
* * - Wildcard (matches everything, captured as group 0, 1, ...)
|
|
138
|
+
* (regex) - Custom regex group
|
|
139
|
+
* :name(regex)- Named parameter with custom regex
|
|
140
|
+
* {pattern}? - Optional group (the ? makes the entire group optional)
|
|
141
|
+
* literal - Literal text (escaped)
|
|
142
|
+
*
|
|
143
|
+
* Returns a CompiledComponent with the original pattern, compiled regex,
|
|
144
|
+
* and an ordered list of group names.
|
|
145
|
+
*/
|
|
146
|
+
function compileComponent(
|
|
147
|
+
pattern: string,
|
|
148
|
+
component: ComponentName,
|
|
149
|
+
): CompiledComponent {
|
|
150
|
+
const groupNames: string[] = [];
|
|
151
|
+
let regexStr = "^";
|
|
152
|
+
let i = 0;
|
|
153
|
+
let unnamedIndex = 0;
|
|
154
|
+
|
|
155
|
+
while (i < pattern.length) {
|
|
156
|
+
const ch = pattern[i];
|
|
157
|
+
|
|
158
|
+
// ----- Optional group: {pattern}? -----
|
|
159
|
+
if (ch === "{") {
|
|
160
|
+
const closeBrace = findMatchingBrace(pattern, i);
|
|
161
|
+
if (closeBrace === -1) {
|
|
162
|
+
// No matching brace, treat as literal
|
|
163
|
+
regexStr += escapeRegExp(ch);
|
|
164
|
+
i++;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const innerPattern = pattern.slice(i + 1, closeBrace);
|
|
169
|
+
const isOptional =
|
|
170
|
+
closeBrace + 1 < pattern.length && pattern[closeBrace + 1] === "?";
|
|
171
|
+
|
|
172
|
+
// Recursively compile the inner pattern
|
|
173
|
+
const inner = compileComponent(innerPattern, component);
|
|
174
|
+
// Strip the ^ and $ anchors from inner regex
|
|
175
|
+
const innerRegex = inner.regex.source.slice(1, -1);
|
|
176
|
+
|
|
177
|
+
if (isOptional) {
|
|
178
|
+
regexStr += `(?:${innerRegex})?`;
|
|
179
|
+
i = closeBrace + 2; // skip }?
|
|
180
|
+
} else {
|
|
181
|
+
regexStr += `(?:${innerRegex})`;
|
|
182
|
+
i = closeBrace + 1; // skip }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Merge inner group names
|
|
186
|
+
for (const name of inner.groupNames) {
|
|
187
|
+
groupNames.push(name);
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ----- Named parameter: :name or :name(regex) -----
|
|
193
|
+
if (ch === ":") {
|
|
194
|
+
i++;
|
|
195
|
+
let name = "";
|
|
196
|
+
while (i < pattern.length && /[\w]/.test(pattern[i])) {
|
|
197
|
+
name += pattern[i];
|
|
198
|
+
i++;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (name === "") {
|
|
202
|
+
// Bare colon, treat as literal
|
|
203
|
+
regexStr += escapeRegExp(":");
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
groupNames.push(name);
|
|
208
|
+
|
|
209
|
+
// Check for custom regex after the name: :name(regex)
|
|
210
|
+
if (i < pattern.length && pattern[i] === "(") {
|
|
211
|
+
const closeParen = findMatchingParen(pattern, i);
|
|
212
|
+
if (closeParen !== -1) {
|
|
213
|
+
const customRegex = pattern.slice(i + 1, closeParen);
|
|
214
|
+
regexStr += `(${customRegex})`;
|
|
215
|
+
i = closeParen + 1;
|
|
216
|
+
} else {
|
|
217
|
+
regexStr += `(${getSegmentWildcard(component)})`;
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
regexStr += `(${getSegmentWildcard(component)})`;
|
|
221
|
+
}
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ----- Wildcard: * -----
|
|
226
|
+
if (ch === "*") {
|
|
227
|
+
groupNames.push(String(unnamedIndex));
|
|
228
|
+
unnamedIndex++;
|
|
229
|
+
regexStr += `(${getFullWildcard(component)})`;
|
|
230
|
+
i++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ----- Custom regex group: (regex) -----
|
|
235
|
+
if (ch === "(") {
|
|
236
|
+
const closeParen = findMatchingParen(pattern, i);
|
|
237
|
+
if (closeParen !== -1) {
|
|
238
|
+
const customRegex = pattern.slice(i + 1, closeParen);
|
|
239
|
+
groupNames.push(String(unnamedIndex));
|
|
240
|
+
unnamedIndex++;
|
|
241
|
+
regexStr += `(${customRegex})`;
|
|
242
|
+
i = closeParen + 1;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// No matching paren, treat as literal
|
|
246
|
+
regexStr += escapeRegExp(ch);
|
|
247
|
+
i++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ----- Literal character -----
|
|
252
|
+
regexStr += escapeRegExp(ch);
|
|
253
|
+
i++;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
regexStr += "$";
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
pattern,
|
|
260
|
+
regex: new RegExp(regexStr),
|
|
261
|
+
groupNames,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Find the matching closing brace for an opening brace at position `start`.
|
|
267
|
+
* Handles nested braces.
|
|
268
|
+
*/
|
|
269
|
+
function findMatchingBrace(str: string, start: number): number {
|
|
270
|
+
let depth = 1;
|
|
271
|
+
let i = start + 1;
|
|
272
|
+
while (i < str.length && depth > 0) {
|
|
273
|
+
if (str[i] === "{") depth++;
|
|
274
|
+
else if (str[i] === "}") depth--;
|
|
275
|
+
if (depth === 0) return i;
|
|
276
|
+
i++;
|
|
277
|
+
}
|
|
278
|
+
return -1;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Find the matching closing parenthesis for an opening paren at position `start`.
|
|
283
|
+
* Handles nested parens and escaped chars.
|
|
284
|
+
*/
|
|
285
|
+
function findMatchingParen(str: string, start: number): number {
|
|
286
|
+
let depth = 1;
|
|
287
|
+
let i = start + 1;
|
|
288
|
+
while (i < str.length && depth > 0) {
|
|
289
|
+
if (str[i] === "\\") {
|
|
290
|
+
i += 2; // Skip escaped character
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (str[i] === "(") depth++;
|
|
294
|
+
else if (str[i] === ")") depth--;
|
|
295
|
+
if (depth === 0) return i;
|
|
296
|
+
i++;
|
|
297
|
+
}
|
|
298
|
+
return -1;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ============================================================================
|
|
302
|
+
// Internal: URL Parsing Helpers
|
|
303
|
+
// ============================================================================
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Parse a URL string (or URLPatternInit) into its components.
|
|
307
|
+
* Returns an object with each component as a string.
|
|
308
|
+
*/
|
|
309
|
+
function parseURLInput(
|
|
310
|
+
input: URLPatternInput,
|
|
311
|
+
baseURL?: string,
|
|
312
|
+
): Record<ComponentName, string> {
|
|
313
|
+
if (typeof input === "object") {
|
|
314
|
+
const result: Record<ComponentName, string> = {
|
|
315
|
+
protocol: "",
|
|
316
|
+
username: "",
|
|
317
|
+
password: "",
|
|
318
|
+
hostname: "",
|
|
319
|
+
port: "",
|
|
320
|
+
pathname: "",
|
|
321
|
+
search: "",
|
|
322
|
+
hash: "",
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// If baseURL is provided, use it as defaults
|
|
326
|
+
if (input.baseURL) {
|
|
327
|
+
const base = parseURLString(input.baseURL);
|
|
328
|
+
for (const comp of COMPONENTS) {
|
|
329
|
+
result[comp] = base[comp];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Override with provided values
|
|
334
|
+
for (const comp of COMPONENTS) {
|
|
335
|
+
if (input[comp] !== undefined) {
|
|
336
|
+
result[comp] = input[comp]!;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// It's a string URL
|
|
344
|
+
const urlStr = baseURL ? resolveURL(input, baseURL) : input;
|
|
345
|
+
return parseURLString(urlStr);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Simple URL resolution: resolve `url` against `base`.
|
|
350
|
+
*/
|
|
351
|
+
function resolveURL(url: string, base: string): string {
|
|
352
|
+
// If url already has a protocol, it's absolute
|
|
353
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {
|
|
354
|
+
return url;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const baseParts = parseURLString(base);
|
|
358
|
+
|
|
359
|
+
if (url.startsWith("//")) {
|
|
360
|
+
// Protocol-relative
|
|
361
|
+
return baseParts.protocol + ":" + url;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (url.startsWith("/")) {
|
|
365
|
+
// Absolute path
|
|
366
|
+
const authority = buildAuthority(baseParts);
|
|
367
|
+
return baseParts.protocol + "://" + authority + url;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Relative path
|
|
371
|
+
const authority = buildAuthority(baseParts);
|
|
372
|
+
const basePath = baseParts.pathname.slice(
|
|
373
|
+
0,
|
|
374
|
+
baseParts.pathname.lastIndexOf("/") + 1,
|
|
375
|
+
);
|
|
376
|
+
return baseParts.protocol + "://" + authority + basePath + url;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Build the authority section (user:pass@host:port) from parsed components.
|
|
381
|
+
*/
|
|
382
|
+
function buildAuthority(parts: Record<ComponentName, string>): string {
|
|
383
|
+
let authority = "";
|
|
384
|
+
if (parts.username) {
|
|
385
|
+
authority += parts.username;
|
|
386
|
+
if (parts.password) {
|
|
387
|
+
authority += ":" + parts.password;
|
|
388
|
+
}
|
|
389
|
+
authority += "@";
|
|
390
|
+
}
|
|
391
|
+
authority += parts.hostname;
|
|
392
|
+
if (parts.port) {
|
|
393
|
+
authority += ":" + parts.port;
|
|
394
|
+
}
|
|
395
|
+
return authority;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Parse a URL string into its component parts.
|
|
400
|
+
* This is a pattern-aware parser that handles both actual URLs and patterns.
|
|
401
|
+
*/
|
|
402
|
+
function parseURLString(url: string): Record<ComponentName, string> {
|
|
403
|
+
const result: Record<ComponentName, string> = {
|
|
404
|
+
protocol: "",
|
|
405
|
+
username: "",
|
|
406
|
+
password: "",
|
|
407
|
+
hostname: "",
|
|
408
|
+
port: "",
|
|
409
|
+
pathname: "",
|
|
410
|
+
search: "",
|
|
411
|
+
hash: "",
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
let remaining = url;
|
|
415
|
+
|
|
416
|
+
// Extract hash
|
|
417
|
+
const hashIdx = remaining.indexOf("#");
|
|
418
|
+
if (hashIdx !== -1) {
|
|
419
|
+
result.hash = remaining.slice(hashIdx + 1);
|
|
420
|
+
remaining = remaining.slice(0, hashIdx);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Extract search
|
|
424
|
+
const searchIdx = remaining.indexOf("?");
|
|
425
|
+
if (searchIdx !== -1) {
|
|
426
|
+
result.search = remaining.slice(searchIdx + 1);
|
|
427
|
+
remaining = remaining.slice(0, searchIdx);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Extract protocol
|
|
431
|
+
const protoMatch = remaining.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):(.*)$/);
|
|
432
|
+
if (protoMatch) {
|
|
433
|
+
result.protocol = protoMatch[1].toLowerCase();
|
|
434
|
+
remaining = protoMatch[2];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Extract authority
|
|
438
|
+
if (remaining.startsWith("//")) {
|
|
439
|
+
remaining = remaining.slice(2);
|
|
440
|
+
|
|
441
|
+
// Find end of authority (starts of path)
|
|
442
|
+
let authEnd = remaining.length;
|
|
443
|
+
const slashIdx = remaining.indexOf("/");
|
|
444
|
+
if (slashIdx !== -1) {
|
|
445
|
+
authEnd = slashIdx;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const authority = remaining.slice(0, authEnd);
|
|
449
|
+
remaining = remaining.slice(authEnd);
|
|
450
|
+
|
|
451
|
+
// Parse userinfo
|
|
452
|
+
const atIdx = authority.lastIndexOf("@");
|
|
453
|
+
let hostPart = authority;
|
|
454
|
+
if (atIdx !== -1) {
|
|
455
|
+
const userinfo = authority.slice(0, atIdx);
|
|
456
|
+
hostPart = authority.slice(atIdx + 1);
|
|
457
|
+
|
|
458
|
+
const colonIdx = userinfo.indexOf(":");
|
|
459
|
+
if (colonIdx !== -1) {
|
|
460
|
+
result.username = userinfo.slice(0, colonIdx);
|
|
461
|
+
result.password = userinfo.slice(colonIdx + 1);
|
|
462
|
+
} else {
|
|
463
|
+
result.username = userinfo;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Parse host:port
|
|
468
|
+
// Handle IPv6
|
|
469
|
+
if (hostPart.startsWith("[")) {
|
|
470
|
+
const bracketEnd = hostPart.indexOf("]");
|
|
471
|
+
if (bracketEnd !== -1) {
|
|
472
|
+
result.hostname = hostPart.slice(0, bracketEnd + 1);
|
|
473
|
+
if (
|
|
474
|
+
bracketEnd + 1 < hostPart.length &&
|
|
475
|
+
hostPart[bracketEnd + 1] === ":"
|
|
476
|
+
) {
|
|
477
|
+
result.port = hostPart.slice(bracketEnd + 2);
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
result.hostname = hostPart;
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
const colonIdx = hostPart.lastIndexOf(":");
|
|
484
|
+
if (colonIdx !== -1) {
|
|
485
|
+
result.hostname = hostPart.slice(0, colonIdx);
|
|
486
|
+
result.port = hostPart.slice(colonIdx + 1);
|
|
487
|
+
} else {
|
|
488
|
+
result.hostname = hostPart;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Remaining is the pathname
|
|
494
|
+
result.pathname = remaining || "/";
|
|
495
|
+
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Parse a pattern string that may contain URL components.
|
|
501
|
+
* Similar to parseURLString but designed for patterns where components
|
|
502
|
+
* may contain wildcard and pattern syntax.
|
|
503
|
+
*/
|
|
504
|
+
function parsePatternString(pattern: string): Record<ComponentName, string> {
|
|
505
|
+
const result: Record<ComponentName, string> = {
|
|
506
|
+
protocol: "*",
|
|
507
|
+
username: "*",
|
|
508
|
+
password: "*",
|
|
509
|
+
hostname: "*",
|
|
510
|
+
port: "*",
|
|
511
|
+
pathname: "*",
|
|
512
|
+
search: "*",
|
|
513
|
+
hash: "*",
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
let remaining = pattern;
|
|
517
|
+
|
|
518
|
+
// Extract hash
|
|
519
|
+
const hashIdx = findUnescapedChar(remaining, "#");
|
|
520
|
+
if (hashIdx !== -1) {
|
|
521
|
+
result.hash = remaining.slice(hashIdx + 1);
|
|
522
|
+
remaining = remaining.slice(0, hashIdx);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Extract search
|
|
526
|
+
const searchIdx = findUnescapedChar(remaining, "?", true);
|
|
527
|
+
if (searchIdx !== -1) {
|
|
528
|
+
result.search = remaining.slice(searchIdx + 1);
|
|
529
|
+
remaining = remaining.slice(0, searchIdx);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Extract protocol
|
|
533
|
+
// Look for a protocol-like pattern: something followed by ://
|
|
534
|
+
// But be careful not to match :name patterns
|
|
535
|
+
const protoEndIdx = remaining.indexOf("://");
|
|
536
|
+
if (protoEndIdx !== -1) {
|
|
537
|
+
result.protocol = remaining.slice(0, protoEndIdx);
|
|
538
|
+
remaining = remaining.slice(protoEndIdx + 3);
|
|
539
|
+
|
|
540
|
+
// Extract authority - everything up to the first /
|
|
541
|
+
const slashIdx = remaining.indexOf("/");
|
|
542
|
+
let authority: string;
|
|
543
|
+
if (slashIdx !== -1) {
|
|
544
|
+
authority = remaining.slice(0, slashIdx);
|
|
545
|
+
remaining = remaining.slice(slashIdx);
|
|
546
|
+
} else {
|
|
547
|
+
authority = remaining;
|
|
548
|
+
remaining = "";
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Parse userinfo@host from authority
|
|
552
|
+
const atIdx = authority.lastIndexOf("@");
|
|
553
|
+
let hostPart = authority;
|
|
554
|
+
if (atIdx !== -1) {
|
|
555
|
+
const userinfo = authority.slice(0, atIdx);
|
|
556
|
+
hostPart = authority.slice(atIdx + 1);
|
|
557
|
+
|
|
558
|
+
const colonIdx = userinfo.indexOf(":");
|
|
559
|
+
if (colonIdx !== -1) {
|
|
560
|
+
result.username = userinfo.slice(0, colonIdx);
|
|
561
|
+
result.password = userinfo.slice(colonIdx + 1);
|
|
562
|
+
} else {
|
|
563
|
+
result.username = userinfo;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Parse host:port - but be careful with patterns
|
|
568
|
+
// We need to find a colon that separates hostname from port,
|
|
569
|
+
// not one that's part of a :name pattern
|
|
570
|
+
const portSep = findPortSeparator(hostPart);
|
|
571
|
+
if (portSep !== -1) {
|
|
572
|
+
result.hostname = hostPart.slice(0, portSep);
|
|
573
|
+
result.port = hostPart.slice(portSep + 1);
|
|
574
|
+
} else {
|
|
575
|
+
result.hostname = hostPart;
|
|
576
|
+
result.port = "*";
|
|
577
|
+
}
|
|
578
|
+
} else {
|
|
579
|
+
// No protocol, might be just a pathname pattern
|
|
580
|
+
// Check if it looks like an absolute path
|
|
581
|
+
if (
|
|
582
|
+
remaining.startsWith("/") ||
|
|
583
|
+
remaining.startsWith(":") ||
|
|
584
|
+
remaining.startsWith("*")
|
|
585
|
+
) {
|
|
586
|
+
result.protocol = "*";
|
|
587
|
+
result.hostname = "*";
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Remaining is pathname
|
|
592
|
+
if (remaining) {
|
|
593
|
+
result.pathname = remaining;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return result;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Find a character in a string, skipping characters inside parentheses and braces.
|
|
601
|
+
* For '?', `topLevelOnly` skips '?' that are part of {pattern}? syntax.
|
|
602
|
+
*/
|
|
603
|
+
function findUnescapedChar(
|
|
604
|
+
str: string,
|
|
605
|
+
ch: string,
|
|
606
|
+
topLevelOnly: boolean = false,
|
|
607
|
+
): number {
|
|
608
|
+
let depth = 0;
|
|
609
|
+
for (let i = 0; i < str.length; i++) {
|
|
610
|
+
const c = str[i];
|
|
611
|
+
if (c === "(" || c === "{") depth++;
|
|
612
|
+
else if (c === ")" || c === "}") depth--;
|
|
613
|
+
else if (c === ch && depth === 0) {
|
|
614
|
+
if (topLevelOnly && ch === "?") {
|
|
615
|
+
// Skip ? that follows } (part of optional syntax)
|
|
616
|
+
if (i > 0 && str[i - 1] === "}") {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return i;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return -1;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Find the colon that separates hostname from port in a host pattern.
|
|
628
|
+
* Must distinguish between `host:port` and `host:name` patterns.
|
|
629
|
+
* A port separator colon is one where the part after it looks like
|
|
630
|
+
* a port (digits, *, or a named param).
|
|
631
|
+
* We scan from the right side.
|
|
632
|
+
*/
|
|
633
|
+
function findPortSeparator(hostPart: string): number {
|
|
634
|
+
// Simple heuristic: find the last colon that's not preceded by
|
|
635
|
+
// the start-of-pattern or another colon (which would make it :name)
|
|
636
|
+
// and where the right side looks like a port pattern.
|
|
637
|
+
|
|
638
|
+
// If the entire string is a wildcard or named param, no port sep
|
|
639
|
+
if (hostPart === "*" || (hostPart.startsWith(":") && !hostPart.includes("."))) {
|
|
640
|
+
return -1;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Look for pattern like hostname:port where port is \d+, *, or :name
|
|
644
|
+
// We look for a colon from the right
|
|
645
|
+
for (let i = hostPart.length - 1; i >= 0; i--) {
|
|
646
|
+
if (hostPart[i] === ":") {
|
|
647
|
+
// Check if this colon is a port separator
|
|
648
|
+
// The part after should look like a port (number, *, :name, or pattern)
|
|
649
|
+
const after = hostPart.slice(i + 1);
|
|
650
|
+
const before = hostPart.slice(0, i);
|
|
651
|
+
|
|
652
|
+
// If 'before' is empty, it's not a port separator
|
|
653
|
+
if (!before) continue;
|
|
654
|
+
|
|
655
|
+
// If 'after' is all digits, *, or starts with :, it's a port
|
|
656
|
+
if (/^\d+$/.test(after) || after === "*" || after.startsWith(":")) {
|
|
657
|
+
return i;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// If 'after' is a pattern in parens, it's a port
|
|
661
|
+
if (after.startsWith("(")) {
|
|
662
|
+
return i;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return -1;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ============================================================================
|
|
671
|
+
// URLPattern Class
|
|
672
|
+
// ============================================================================
|
|
673
|
+
|
|
674
|
+
export class URLPattern {
|
|
675
|
+
private _components: Record<ComponentName, CompiledComponent>;
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Create a new URLPattern.
|
|
679
|
+
*
|
|
680
|
+
* @param input - A pattern string (like "https://example.com/:path") or
|
|
681
|
+
* an object with component patterns ({pathname: "/users/:id"})
|
|
682
|
+
* @param baseURL - Optional base URL string (only when input is a string)
|
|
683
|
+
*/
|
|
684
|
+
constructor(input: URLPatternInput, baseURL?: string) {
|
|
685
|
+
let patterns: Record<ComponentName, string>;
|
|
686
|
+
|
|
687
|
+
if (typeof input === "string") {
|
|
688
|
+
// Parse the pattern string into components
|
|
689
|
+
if (baseURL) {
|
|
690
|
+
// If baseURL is provided, parse the base first, then overlay the input
|
|
691
|
+
const baseParts = parsePatternString(baseURL);
|
|
692
|
+
const inputParts = parsePatternString(input);
|
|
693
|
+
|
|
694
|
+
// The input overrides the base for components it specifies
|
|
695
|
+
patterns = { ...baseParts };
|
|
696
|
+
for (const comp of COMPONENTS) {
|
|
697
|
+
if (inputParts[comp] !== "*" || comp === "pathname") {
|
|
698
|
+
patterns[comp] = inputParts[comp];
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
} else {
|
|
702
|
+
patterns = parsePatternString(input);
|
|
703
|
+
}
|
|
704
|
+
} else {
|
|
705
|
+
// Object input - use defaults for unspecified components
|
|
706
|
+
patterns = { ...DEFAULT_PATTERNS };
|
|
707
|
+
|
|
708
|
+
// If baseURL is provided in the init object, parse it first
|
|
709
|
+
if (input.baseURL) {
|
|
710
|
+
const baseParts = parsePatternString(input.baseURL);
|
|
711
|
+
for (let i = 0; i < COMPONENTS.length; i++) {
|
|
712
|
+
const comp = COMPONENTS[i];
|
|
713
|
+
patterns[comp] = baseParts[comp];
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Override with explicitly provided values
|
|
718
|
+
for (let i = 0; i < COMPONENTS.length; i++) {
|
|
719
|
+
const comp = COMPONENTS[i];
|
|
720
|
+
if (input[comp] !== undefined) {
|
|
721
|
+
patterns[comp] = input[comp]!;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Compile each component pattern into a regex
|
|
727
|
+
this._components = {} as Record<ComponentName, CompiledComponent>;
|
|
728
|
+
for (let i = 0; i < COMPONENTS.length; i++) {
|
|
729
|
+
const comp = COMPONENTS[i];
|
|
730
|
+
this._components[comp] = compileComponent(patterns[comp], comp);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* The compiled pattern string for the protocol component.
|
|
736
|
+
*/
|
|
737
|
+
get protocol(): string {
|
|
738
|
+
return this._components.protocol.pattern;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* The compiled pattern string for the username component.
|
|
743
|
+
*/
|
|
744
|
+
get username(): string {
|
|
745
|
+
return this._components.username.pattern;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* The compiled pattern string for the password component.
|
|
750
|
+
*/
|
|
751
|
+
get password(): string {
|
|
752
|
+
return this._components.password.pattern;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* The compiled pattern string for the hostname component.
|
|
757
|
+
*/
|
|
758
|
+
get hostname(): string {
|
|
759
|
+
return this._components.hostname.pattern;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* The compiled pattern string for the port component.
|
|
764
|
+
*/
|
|
765
|
+
get port(): string {
|
|
766
|
+
return this._components.port.pattern;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* The compiled pattern string for the pathname component.
|
|
771
|
+
*/
|
|
772
|
+
get pathname(): string {
|
|
773
|
+
return this._components.pathname.pattern;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* The compiled pattern string for the search component.
|
|
778
|
+
*/
|
|
779
|
+
get search(): string {
|
|
780
|
+
return this._components.search.pattern;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* The compiled pattern string for the hash component.
|
|
785
|
+
*/
|
|
786
|
+
get hash(): string {
|
|
787
|
+
return this._components.hash.pattern;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Test whether a URL matches this pattern.
|
|
792
|
+
*
|
|
793
|
+
* @param input - A URL string or URLPatternInit to test
|
|
794
|
+
* @param baseURL - Optional base URL (only when input is a string)
|
|
795
|
+
* @returns true if the URL matches the pattern
|
|
796
|
+
*/
|
|
797
|
+
test(input: URLPatternInput, baseURL?: string): boolean {
|
|
798
|
+
return this.exec(input, baseURL) !== null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Execute this pattern against a URL, returning match details or null.
|
|
803
|
+
*
|
|
804
|
+
* @param input - A URL string or URLPatternInit to match
|
|
805
|
+
* @param baseURL - Optional base URL (only when input is a string)
|
|
806
|
+
* @returns URLPatternResult with match details, or null if no match
|
|
807
|
+
*/
|
|
808
|
+
exec(input: URLPatternInput, baseURL?: string): URLPatternResult | null {
|
|
809
|
+
let urlComponents: Record<ComponentName, string>;
|
|
810
|
+
|
|
811
|
+
try {
|
|
812
|
+
urlComponents = parseURLInput(input, baseURL);
|
|
813
|
+
} catch {
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const result: Partial<URLPatternResult> = {};
|
|
818
|
+
|
|
819
|
+
// Build inputs array
|
|
820
|
+
if (baseURL) {
|
|
821
|
+
(result as any).inputs = [input, baseURL];
|
|
822
|
+
} else {
|
|
823
|
+
(result as any).inputs = [input];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Match each component
|
|
827
|
+
for (const comp of COMPONENTS) {
|
|
828
|
+
const compiled = this._components[comp];
|
|
829
|
+
const value = urlComponents[comp];
|
|
830
|
+
const match = compiled.regex.exec(value);
|
|
831
|
+
|
|
832
|
+
if (!match) {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Build groups from captures
|
|
837
|
+
const groups: Record<string, string | undefined> = {};
|
|
838
|
+
for (let i = 0; i < compiled.groupNames.length; i++) {
|
|
839
|
+
groups[compiled.groupNames[i]] = match[i + 1];
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
(result as any)[comp] = {
|
|
843
|
+
input: value,
|
|
844
|
+
groups,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
return result as URLPatternResult;
|
|
849
|
+
}
|
|
850
|
+
}
|