xbintsc 0.5.37 → 0.5.38

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.
Files changed (68) hide show
  1. package/dist/src/cli/help.d.ts +1 -1
  2. package/dist/src/codegen/generator/state.js +1 -0
  3. package/dist/src/codegen/generator/state.js.map +1 -1
  4. package/dist/src/codegen/generator/tables.js +1 -0
  5. package/dist/src/codegen/generator/tables.js.map +1 -1
  6. package/dist/src/driver/bundler/deferred.d.ts +63 -0
  7. package/dist/src/driver/bundler/deferred.js +279 -0
  8. package/dist/src/driver/bundler/deferred.js.map +1 -0
  9. package/dist/src/driver/bundler/merge.js +214 -2
  10. package/dist/src/driver/bundler/merge.js.map +1 -1
  11. package/dist/src/driver/runtime-objects.d.ts +1 -1
  12. package/dist/src/driver/runtime-objects.js +1 -0
  13. package/dist/src/driver/runtime-objects.js.map +1 -1
  14. package/dist/src/driver/version.d.ts +1 -1
  15. package/dist/src/driver/version.js +1 -1
  16. package/dist/src/extensions/gui/html.js +89 -13
  17. package/dist/src/extensions/gui/html.js.map +1 -1
  18. package/dist/src/extensions/gui/index.js +1 -0
  19. package/dist/src/extensions/gui/index.js.map +1 -1
  20. package/dist/src/extensions/gui/three.d.ts +4 -3
  21. package/dist/src/extensions/gui/three.js +4 -3
  22. package/dist/src/extensions/gui/three.js.map +1 -1
  23. package/dist/tests/e2e/gui-script-import-order.test.d.ts +10 -0
  24. package/dist/tests/e2e/gui-script-import-order.test.js +161 -0
  25. package/dist/tests/e2e/gui-script-import-order.test.js.map +1 -0
  26. package/dist/tests/e2e/gui-webgl2-shadow.test.d.ts +10 -0
  27. package/dist/tests/e2e/gui-webgl2-shadow.test.js +108 -0
  28. package/dist/tests/e2e/gui-webgl2-shadow.test.js.map +1 -0
  29. package/dist/tests/e2e/url.test.d.ts +6 -0
  30. package/dist/tests/e2e/url.test.js +114 -0
  31. package/dist/tests/e2e/url.test.js.map +1 -0
  32. package/dist/tests/extensions/gui.test.js +22 -10
  33. package/dist/tests/extensions/gui.test.js.map +1 -1
  34. package/doc/gui/webgl.md +20 -10
  35. package/doc/gui-scripts.md +19 -19
  36. package/doc/gui.md +10 -8
  37. package/doc/implemented/fetch.md +5 -0
  38. package/doc/implemented/modules.md +13 -0
  39. package/doc/implemented/stdlib.md +17 -0
  40. package/doc/unimplemented/deviations.md +1 -0
  41. package/doc/zh-CN/gui/webgl.md +3 -3
  42. package/doc/zh-CN/gui-scripts.md +14 -13
  43. package/doc/zh-CN/gui.md +7 -6
  44. package/doc/zh-CN/implemented/fetch.md +4 -0
  45. package/doc/zh-CN/implemented/modules.md +10 -0
  46. package/doc/zh-CN/implemented/stdlib.md +15 -0
  47. package/doc/zh-CN/unimplemented/deviations.md +1 -0
  48. package/package.json +7 -7
  49. package/runtime/ext_gui/dom_api_webgl.cpp +10 -0
  50. package/runtime/ext_gui/dom_api_webgl2.cpp +33 -1
  51. package/runtime/ext_gui/gui_engine.h +2 -0
  52. package/runtime/ext_gui/script.cpp +31 -0
  53. package/runtime/ext_gui/webgl_glsl.h +3 -0
  54. package/runtime/ext_gui/webgl_glsl_unroll.cpp +283 -0
  55. package/runtime/ext_gui/webgl_msl.cpp +4 -0
  56. package/runtime/ext_gui/webgl_shader.cpp +12 -6
  57. package/runtime/rt.h +3 -0
  58. package/runtime/xt_fetch/async.inc +60 -0
  59. package/runtime/xt_url_object.c +463 -0
  60. package/src/codegen/generator/state.ts +1 -0
  61. package/src/codegen/generator/tables.ts +1 -0
  62. package/src/driver/bundler/deferred.ts +351 -0
  63. package/src/driver/bundler/merge.ts +219 -2
  64. package/src/driver/runtime-objects.ts +1 -0
  65. package/src/driver/version.ts +1 -1
  66. package/src/extensions/gui/html.ts +91 -15
  67. package/src/extensions/gui/index.ts +1 -0
  68. package/src/extensions/gui/three.ts +4 -3
@@ -0,0 +1,463 @@
1
+ /*
2
+ * xbintsc runtime — URL.
3
+ *
4
+ * Relative-reference resolution against a base URL, plus the read-only
5
+ * components a compiled page or CLI tool typically reads. The WHATWG URL
6
+ * constructor is `URL`; `URLSearchParams` lives in xt_url.c.
7
+ *
8
+ * A reference with no scheme is a filesystem path: it resolves against a
9
+ * `file://` base (the working directory when the caller supplied no base), so
10
+ * `new URL("./model.glb", import.meta.url)` and `new URL("./model.glb")`
11
+ * both produce a usable file URL. `fetch` uses the same rule for relative
12
+ * requests (see `xt_url_file_path`), which is how a compiled page can load a
13
+ * local asset without going through an HTTP server.
14
+ */
15
+
16
+ #include "rt_internal.h"
17
+
18
+ #include <ctype.h>
19
+ #include <stdio.h>
20
+ #include <stdlib.h>
21
+ #include <string.h>
22
+
23
+ #if defined(_WIN32)
24
+ #include <direct.h>
25
+ #define xt_url_getcwd _getcwd
26
+ #else
27
+ #include <unistd.h>
28
+ #define xt_url_getcwd getcwd
29
+ #endif
30
+
31
+ /* URLSearchParams is defined in xt_url.c. */
32
+ extern xt_value xt_url_search_params_ctor(int32_t argc, xt_value *argv);
33
+
34
+ /* -- URL ------------------------------------------------------------------ */
35
+
36
+ /*
37
+ * A deliberately small WHATWG-ish `URL`: resolution of relative references
38
+ * against a base (the shape a browser page and `import.meta.url` produce),
39
+ * plus the read-only components the games and tools we compile actually use.
40
+ * It is not a full URL parser: percent-encoding of the path, IDNA hosts and
41
+ * the `URL` setters are out of scope.
42
+ */
43
+
44
+ /** Longest URL this runtime will build. Longer inputs are truncated. */
45
+ #define XT_URL_MAX 8192
46
+
47
+ /** Length of a leading `scheme:` prefix, or 0 when `text` has no scheme. */
48
+ static size_t xt_url_scheme_length(const char *text) {
49
+ if (!isalpha((unsigned char)text[0])) return 0;
50
+ size_t i = 1;
51
+ for (; text[i]; i++) {
52
+ if (text[i] == ':') {
53
+ #if defined(_WIN32)
54
+ /* A drive letter (`C:\dir` / `C:/dir`) is a path, not a scheme. */
55
+ if (i == 1 && (text[2] == '\\' || text[2] == '/')) return 0;
56
+ #endif
57
+ return i;
58
+ }
59
+ unsigned char c = (unsigned char)text[i];
60
+ if (!(isalnum(c) || c == '+' || c == '-' || c == '.')) return 0;
61
+ }
62
+ return 0;
63
+ }
64
+
65
+ /** Join `scheme://authority` (already written) with an absolute path. */
66
+ static void xt_url_append(char *out, size_t size, const char *text) {
67
+ size_t used = strlen(out);
68
+ size_t length = strlen(text);
69
+ if (used + length >= size) length = used < size ? size - used - 1 : 0;
70
+ memcpy(out + used, text, length);
71
+ out[used + length] = 0;
72
+ }
73
+
74
+ /** Normalize `.` and `..` segments in `path` into `out`. */
75
+ static void xt_url_normalize_path(const char *path, char *out, size_t size) {
76
+ int absolute = path[0] == '/';
77
+ size_t length = strlen(path);
78
+ int trailing = length > 0 && path[length - 1] == '/';
79
+ size_t write = 0;
80
+ if (absolute && write + 1 < size) out[write++] = '/';
81
+ const char *cursor = path;
82
+ while (*cursor == '/') cursor++;
83
+ while (*cursor) {
84
+ const char *start = cursor;
85
+ while (*cursor && *cursor != '/') cursor++;
86
+ size_t segment = (size_t)(cursor - start);
87
+ if (segment == 1 && start[0] == '.') {
88
+ /* `./` adds nothing. */
89
+ } else if (segment == 2 && start[0] == '.' && start[1] == '.') {
90
+ size_t floor = absolute ? 1 : 0;
91
+ if (write > floor) {
92
+ while (write > floor && out[write - 1] != '/') write--;
93
+ if (write > floor && out[write - 1] == '/') write--;
94
+ }
95
+ } else if (segment > 0) {
96
+ if (write > 0 && out[write - 1] != '/' && write + 1 < size) out[write++] = '/';
97
+ size_t copy = segment;
98
+ if (write + copy >= size) copy = write < size ? size - write - 1 : 0;
99
+ memcpy(out + write, start, copy);
100
+ write += copy;
101
+ }
102
+ while (*cursor == '/') cursor++;
103
+ }
104
+ if (trailing && write > 0 && out[write - 1] != '/' && write + 1 < size) out[write++] = '/';
105
+ if (write == 0 && size > 1) out[write++] = '/';
106
+ out[write < size ? write : size - 1] = 0;
107
+ }
108
+
109
+ /** The `file://` URL of the working directory, with a trailing slash. */
110
+ static void xt_url_cwd_base(char *out, size_t size) {
111
+ char cwd[4096];
112
+ cwd[0] = 0;
113
+ if (xt_url_getcwd(cwd, sizeof(cwd)) == NULL) cwd[0] = 0;
114
+ size_t length = strlen(cwd);
115
+ while (length > 1 && (cwd[length - 1] == '/' || cwd[length - 1] == '\\')) cwd[--length] = 0;
116
+ /* Windows reports `C:\dir`; a file URL spells that `/C:/dir`. */
117
+ char path[4300];
118
+ size_t j = 0;
119
+ #if defined(_WIN32)
120
+ if (length > 0 && cwd[0] != '/') path[j++] = '/';
121
+ #endif
122
+ for (size_t i = 0; i < length && j + 1 < sizeof(path); i++) {
123
+ path[j++] = cwd[i] == '\\' ? '/' : cwd[i];
124
+ }
125
+ path[j] = 0;
126
+ if (j == 0 || path[j - 1] != '/') {
127
+ if (j + 1 < sizeof(path)) {
128
+ path[j++] = '/';
129
+ path[j] = 0;
130
+ }
131
+ }
132
+ snprintf(out, size, "file://%s", path);
133
+ }
134
+
135
+ /** Build `href` for `input` resolved against `base` (either may be empty). */
136
+ static void xt_url_resolve(const char *input, const char *base, char *out, size_t size) {
137
+ out[0] = 0;
138
+ if (input[0] == 0) {
139
+ snprintf(out, size, "%s", base);
140
+ return;
141
+ }
142
+ if (xt_url_scheme_length(input) > 0) {
143
+ snprintf(out, size, "%s", input);
144
+ return;
145
+ }
146
+ if (base[0] == 0) {
147
+ snprintf(out, size, "%s", input);
148
+ return;
149
+ }
150
+
151
+ size_t schemeLength = xt_url_scheme_length(base);
152
+ if (schemeLength == 0) {
153
+ snprintf(out, size, "%s", input);
154
+ return;
155
+ }
156
+ int hasAuthority = base[schemeLength + 1] == '/' && base[schemeLength + 2] == '/';
157
+ if (input[0] == '/' && input[1] == '/') {
158
+ snprintf(out, size, "%.*s:%s", (int)schemeLength, base, input);
159
+ return;
160
+ }
161
+ const char *authorityStart = base + schemeLength + (hasAuthority ? 3 : 1);
162
+ const char *authorityEnd = authorityStart;
163
+ while (*authorityEnd && *authorityEnd != '/' && *authorityEnd != '?' && *authorityEnd != '#') authorityEnd++;
164
+ const char *pathStart = authorityEnd;
165
+ if (*pathStart != '/') pathStart = "";
166
+
167
+ /* scheme: + //authority — the fixed prefix every resolved URL keeps. */
168
+ size_t prefix = 0;
169
+ if (prefix + schemeLength + 1 < size) {
170
+ memcpy(out + prefix, base, schemeLength);
171
+ prefix += schemeLength;
172
+ out[prefix++] = ':';
173
+ }
174
+ if (hasAuthority) {
175
+ if (prefix + 2 < size) {
176
+ out[prefix++] = '/';
177
+ out[prefix++] = '/';
178
+ }
179
+ size_t authorityLength = (size_t)(authorityEnd - authorityStart);
180
+ if (prefix + authorityLength >= size) authorityLength = size - prefix - 1;
181
+ memcpy(out + prefix, authorityStart, authorityLength);
182
+ prefix += authorityLength;
183
+ }
184
+ out[prefix] = 0;
185
+
186
+ /* Split the reference into its path and its `?query#fragment` suffix. */
187
+ const char *suffix = input;
188
+ while (*suffix && *suffix != '?' && *suffix != '#') suffix++;
189
+ size_t inputPathLength = (size_t)(suffix - input);
190
+
191
+ char joined[XT_URL_MAX];
192
+ joined[0] = 0;
193
+ if (input[0] == '/') {
194
+ if (inputPathLength >= sizeof(joined)) inputPathLength = sizeof(joined) - 1;
195
+ memcpy(joined, input, inputPathLength);
196
+ joined[inputPathLength] = 0;
197
+ } else {
198
+ size_t basePathLength = 0;
199
+ const char *basePathEnd = pathStart;
200
+ while (*basePathEnd && *basePathEnd != '?' && *basePathEnd != '#') basePathEnd++;
201
+ basePathLength = (size_t)(basePathEnd - pathStart);
202
+ size_t directory = 0;
203
+ for (size_t i = 0; i < basePathLength; i++) {
204
+ if (pathStart[i] == '/') directory = i + 1;
205
+ }
206
+ if (directory == 0 && hasAuthority) directory = 0;
207
+ if (directory >= sizeof(joined)) directory = sizeof(joined) - 1;
208
+ memcpy(joined, pathStart, directory);
209
+ joined[directory] = 0;
210
+ size_t room = sizeof(joined) - directory - 1;
211
+ if (inputPathLength > room) inputPathLength = room;
212
+ memcpy(joined + directory, input, inputPathLength);
213
+ joined[directory + inputPathLength] = 0;
214
+ }
215
+
216
+ char normalized[XT_URL_MAX];
217
+ xt_url_normalize_path(joined, normalized, sizeof(normalized));
218
+ xt_url_append(out, size, normalized);
219
+ xt_url_append(out, size, suffix);
220
+ }
221
+
222
+ /**
223
+ * Canonicalize the Windows spellings of a `file:` URL. `file://C:/dir` and
224
+ * `file://C:\dir` both name the path `C:\dir`, and a special URL treats a
225
+ * backslash as a path separator, so both become `file:///C:/dir` — the same
226
+ * href `xt_url_cwd_base` produces and the same one Node reports. Anything
227
+ * else (including a real host) is copied unchanged.
228
+ */
229
+ static void xt_url_canonical_file(const char *url, char *out, size_t size) {
230
+ snprintf(out, size, "%s", url);
231
+ if (xt_url_scheme_length(url) != 4 || strncmp(url, "file:", 5) != 0) return;
232
+ char normalized[XT_URL_MAX];
233
+ snprintf(normalized, sizeof(normalized), "%s", url);
234
+ for (char *p = normalized; *p && *p != '?' && *p != '#'; p++) {
235
+ if (*p == '\\') *p = '/';
236
+ }
237
+ const char *rest = normalized + 5;
238
+ if (rest[0] != '/' || rest[1] != '/') return;
239
+ const char *authority = rest + 2;
240
+ const char *end = authority;
241
+ while (*end && *end != '/' && *end != '?' && *end != '#') end++;
242
+ if (end - authority != 2 || !isalpha((unsigned char)authority[0]) || authority[1] != ':') return;
243
+ snprintf(out, size, "file:///%.*s%s", (int)(end - authority), authority, end);
244
+ }
245
+
246
+ /** Publish the read-only components parsed from `href` on the URL object. */
247
+ static void xt_url_set_components(xt_value url, const char *href) {
248
+ size_t schemeLength = xt_url_scheme_length(href);
249
+ char scheme[32];
250
+ if (schemeLength >= sizeof(scheme)) schemeLength = sizeof(scheme) - 1;
251
+ memcpy(scheme, href, schemeLength);
252
+ scheme[schemeLength] = 0;
253
+ char protocol[36];
254
+ snprintf(protocol, sizeof(protocol), "%s:", scheme);
255
+
256
+ const char *rest = href + schemeLength + (schemeLength > 0 && href[schemeLength] == ':' ? 1 : 0);
257
+ int hasAuthority = rest[0] == '/' && rest[1] == '/';
258
+ const char *authorityStart = rest + (hasAuthority ? 2 : 0);
259
+ const char *authorityEnd = authorityStart;
260
+ while (*authorityEnd && *authorityEnd != '/' && *authorityEnd != '?' && *authorityEnd != '#') authorityEnd++;
261
+ const char *pathStart = authorityEnd;
262
+ const char *pathEnd = pathStart;
263
+ while (*pathEnd && *pathEnd != '?' && *pathEnd != '#') pathEnd++;
264
+ const char *search = (*pathEnd == '?') ? pathEnd : "";
265
+ const char *searchEnd = search;
266
+ if (*search) while (*searchEnd && *searchEnd != '#') searchEnd++;
267
+ const char *hash = (*searchEnd == '#') ? searchEnd : (pathEnd && *pathEnd == '#' ? pathEnd : "");
268
+
269
+ char authority[512];
270
+ size_t authorityLength = (size_t)(authorityEnd - authorityStart);
271
+ if (authorityLength >= sizeof(authority)) authorityLength = sizeof(authority) - 1;
272
+ memcpy(authority, authorityStart, authorityLength);
273
+ authority[authorityLength] = 0;
274
+
275
+ char hostname[512];
276
+ snprintf(hostname, sizeof(hostname), "%s", authority);
277
+ char *colon = strrchr(hostname, ':');
278
+ char port[16] = "";
279
+ if (colon && (colon > hostname) && (colon[1] == 0 || (colon[1] >= '0' && colon[1] <= '9'))) {
280
+ snprintf(port, sizeof(port), "%s", colon + 1);
281
+ *colon = 0;
282
+ }
283
+ char pathname[XT_URL_MAX];
284
+ size_t pathLength = (size_t)(pathEnd - pathStart);
285
+ if (pathLength >= sizeof(pathname)) pathLength = sizeof(pathname) - 1;
286
+ memcpy(pathname, pathStart, pathLength);
287
+ pathname[pathLength] = 0;
288
+ char searchText[XT_URL_MAX];
289
+ size_t searchLength = (size_t)(searchEnd - search);
290
+ if (searchLength >= sizeof(searchText)) searchLength = sizeof(searchText) - 1;
291
+ memcpy(searchText, search, searchLength);
292
+ searchText[searchLength] = 0;
293
+ char hashText[XT_URL_MAX];
294
+ snprintf(hashText, sizeof(hashText), "%s", hash);
295
+ char origin[XT_URL_MAX];
296
+ if (!hasAuthority) snprintf(origin, sizeof(origin), "%s", protocol);
297
+ else if (strcmp(scheme, "file") == 0) snprintf(origin, sizeof(origin), "null");
298
+ else snprintf(origin, sizeof(origin), "%s://%s", scheme, authority);
299
+
300
+ xt_object_set(url, xt_string_from_cstr("href"), xt_string_from_cstr(href));
301
+ xt_object_set(url, xt_string_from_cstr("protocol"), xt_string_from_cstr(protocol));
302
+ xt_object_set(url, xt_string_from_cstr("origin"), xt_string_from_cstr(origin));
303
+ xt_object_set(url, xt_string_from_cstr("host"), xt_string_from_cstr(authority));
304
+ xt_object_set(url, xt_string_from_cstr("hostname"), xt_string_from_cstr(hostname));
305
+ xt_object_set(url, xt_string_from_cstr("port"), xt_string_from_cstr(port));
306
+ xt_object_set(url, xt_string_from_cstr("pathname"), xt_string_from_cstr(pathname));
307
+ xt_object_set(url, xt_string_from_cstr("search"), xt_string_from_cstr(searchText));
308
+ xt_object_set(url, xt_string_from_cstr("hash"), xt_string_from_cstr(hashText));
309
+
310
+ /* `searchParams` is a snapshot of `search`; the WHATWG live link between the
311
+ * two is out of scope, like the `URL` setters. */
312
+ xt_value query = xt_string_from_cstr(searchText);
313
+ xt_object_set(url, xt_string_from_cstr("searchParams"), xt_url_search_params_ctor(1, &query));
314
+ }
315
+
316
+ static xt_value xt_url_method_href(xt_value thisValue, xt_value env, int32_t argc, xt_value *argv) {
317
+ (void)env;
318
+ (void)argc;
319
+ (void)argv;
320
+ return xt_object_get(thisValue, xt_string_from_cstr("href"));
321
+ }
322
+
323
+ xt_value xt_url_ctor(int32_t argc, xt_value *argv) {
324
+ /* `new URL(input, base)`: a base may itself be a URL object. */
325
+ xt_value inputValue = xt_arg_at(argc, argv, 0);
326
+ xt_value baseValue = xt_arg_at(argc, argv, 1);
327
+ xt_value input = inputValue == XT_UNDEFINED ? xt_string_from_cstr("") : xt_to_string(inputValue);
328
+ char base[XT_URL_MAX];
329
+ base[0] = 0;
330
+ if (baseValue != XT_UNDEFINED) {
331
+ xt_value href = XT_IS_OBJECT(baseValue)
332
+ ? xt_object_get(baseValue, xt_string_from_cstr("href"))
333
+ : xt_to_string(baseValue);
334
+ snprintf(base, sizeof(base), "%s", href == XT_UNDEFINED ? "" : xt_string_data(href));
335
+ }
336
+ const char *inputText = xt_string_data(input);
337
+ if (!inputText) inputText = "";
338
+
339
+ /* A reference without a scheme is a filesystem path. With no explicit base
340
+ * it resolves against the working directory, so `new URL("./a.txt")` is a
341
+ * `file://` URL rather than an unparseable relative one. */
342
+ char adjusted[XT_URL_MAX];
343
+ if (base[0] == 0 && xt_url_scheme_length(inputText) == 0) {
344
+ xt_url_cwd_base(base, sizeof(base));
345
+ }
346
+ #if defined(_WIN32)
347
+ if (xt_url_scheme_length(inputText) == 0 && isalpha((unsigned char)inputText[0]) &&
348
+ inputText[1] == ':' && (inputText[2] == '\\' || inputText[2] == '/')) {
349
+ /* `C:\dir\file` -> `/C:/dir/file`, the path part of a file URL. */
350
+ size_t j = 0;
351
+ adjusted[j++] = '/';
352
+ for (size_t i = 0; inputText[i] && j + 1 < sizeof(adjusted); i++) {
353
+ adjusted[j++] = inputText[i] == '\\' ? '/' : inputText[i];
354
+ }
355
+ adjusted[j] = 0;
356
+ inputText = adjusted;
357
+ }
358
+ #endif
359
+
360
+ char canonical[XT_URL_MAX];
361
+ xt_url_canonical_file(inputText, canonical, sizeof(canonical));
362
+ inputText = canonical;
363
+
364
+ char href[XT_URL_MAX];
365
+ xt_url_resolve(inputText, base, href, sizeof(href));
366
+ xt_value url = xt_object_new();
367
+ xt_url_set_components(url, href);
368
+ xt_object_set(url, xt_string_from_cstr("toString"),
369
+ xt_closure_new((void *)xt_url_method_href, 0, NULL));
370
+ xt_object_set(url, xt_string_from_cstr("toJSON"),
371
+ xt_closure_new((void *)xt_url_method_href, 0, NULL));
372
+ return url;
373
+ }
374
+
375
+ /* -- filesystem paths ----------------------------------------------------- */
376
+
377
+ static int xt_url_hex(char c) {
378
+ if (c >= '0' && c <= '9') return c - '0';
379
+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
380
+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
381
+ return -1;
382
+ }
383
+
384
+ /** Append the percent-decoded `text` to `out` (a decoded `%2F` stays escaped). */
385
+ static void xt_url_append_decoded(char *out, size_t size, const char *text, size_t length) {
386
+ size_t used = strlen(out);
387
+ for (size_t i = 0; i < length && used + 2 < size; i++) {
388
+ if (text[i] == '%' && i + 2 < length && xt_url_hex(text[i + 1]) >= 0 && xt_url_hex(text[i + 2]) >= 0) {
389
+ out[used++] = (char)((xt_url_hex(text[i + 1]) << 4) | xt_url_hex(text[i + 2]));
390
+ i += 2;
391
+ } else {
392
+ out[used++] = text[i] == '\\' ? '/' : text[i];
393
+ }
394
+ }
395
+ out[used] = 0;
396
+ }
397
+
398
+ /**
399
+ * Turn a `fetch` reference into a filesystem path.
400
+ *
401
+ * Returns 1 and writes an absolute path for `file:` URLs and for relative
402
+ * references with no scheme (resolved against the working directory), 0 for
403
+ * another scheme. The `?query` / `#fragment` suffix is not part of the path.
404
+ */
405
+ int xt_url_file_path(const char *url, char *out, size_t size) {
406
+ if (!url || url[0] == '\0') return 0;
407
+ out[0] = 0;
408
+ char work[XT_URL_MAX];
409
+ snprintf(work, sizeof(work), "%s", url);
410
+ char *fragment = work;
411
+ while (*fragment && *fragment != '?' && *fragment != '#') fragment++;
412
+ *fragment = 0;
413
+ /* A reference that is only a query/fragment has no file to read; let the
414
+ * caller report it as an invalid URL like an empty `fetch("")`. */
415
+ if (work[0] == '\0') return 0;
416
+
417
+ size_t schemeLength = xt_url_scheme_length(work);
418
+ const char *path = work;
419
+ if (schemeLength > 0) {
420
+ if (schemeLength != 4 || strncmp(work, "file:", 5) != 0) return 0;
421
+ /* `file://C:\dir` is the Windows spelling of `file:///C:/dir`; rewrite it
422
+ * before separating the authority so the drive is not mistaken for a host
423
+ * (and a backslash is a separator, as in a browser). */
424
+ char canonical[XT_URL_MAX];
425
+ xt_url_canonical_file(work, canonical, sizeof(canonical));
426
+ path = canonical + 5;
427
+ if (path[0] == '/' && path[1] == '/') {
428
+ path += 2;
429
+ const char *slash = strchr(path, '/');
430
+ if (slash != NULL) {
431
+ size_t hostLength = (size_t)(slash - path);
432
+ int localhost = hostLength == 9 && strncmp(path, "localhost", 9) == 0;
433
+ if (hostLength > 0 && !localhost) return 0;
434
+ path = slash;
435
+ } else {
436
+ path = "/";
437
+ }
438
+ }
439
+ xt_url_append_decoded(out, size, path, strlen(path));
440
+ } else {
441
+ /* Relative references are relative to the process working directory. */
442
+ char cwd[4096];
443
+ cwd[0] = 0;
444
+ if (xt_url_getcwd(cwd, sizeof(cwd)) == NULL) cwd[0] = 0;
445
+ int absolute = work[0] == '/' || work[0] == '\\';
446
+ #if defined(_WIN32)
447
+ if (!absolute && isalpha((unsigned char)work[0]) && work[1] == ':' &&
448
+ (work[2] == '/' || work[2] == '\\')) {
449
+ absolute = 1;
450
+ }
451
+ #endif
452
+ if (!absolute && cwd[0] != 0) {
453
+ snprintf(out, size, "%s/", cwd);
454
+ }
455
+ xt_url_append_decoded(out, size, work, strlen(work));
456
+ }
457
+ #if defined(_WIN32)
458
+ if (out[0] == '/' && isalpha((unsigned char)out[1]) && out[2] == ':') {
459
+ memmove(out, out + 1, strlen(out));
460
+ }
461
+ #endif
462
+ return out[0] != 0;
463
+ }
@@ -200,6 +200,7 @@ export const RUNTIME_DECLARATIONS: readonly string[] = [
200
200
  "declare i64 @xt_text_decoder_ctor(i32, i64*)",
201
201
  "declare i64 @xt_date_ctor(i32, i64*)",
202
202
  "declare i64 @xt_regexp_ctor(i32, i64*)",
203
+ "declare i64 @xt_url_ctor(i32, i64*)",
203
204
  "declare i64 @xt_url_search_params_ctor(i32, i64*)",
204
205
  "declare i64 @xt_array_buffer_ctor(i32, i64*)",
205
206
  "declare i64 @xt_array_buffer_static(i64, i32, i64*)",
@@ -316,6 +316,7 @@ export const CTOR_FUNCTIONS: Record<string, string> = {
316
316
  Socket: "xt_net_socket_ctor",
317
317
  Server: "xt_net_server_ctor",
318
318
  EventEmitter: "xt_event_emitter_ctor",
319
+ URL: "xt_url_ctor",
319
320
  URLSearchParams: "xt_url_search_params_ctor",
320
321
  TextEncoder: "xt_text_encoder_ctor",
321
322
  TextDecoder: "xt_text_decoder_ctor",