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.
- package/dist/src/cli/help.d.ts +1 -1
- package/dist/src/codegen/generator/state.js +1 -0
- package/dist/src/codegen/generator/state.js.map +1 -1
- package/dist/src/codegen/generator/tables.js +1 -0
- package/dist/src/codegen/generator/tables.js.map +1 -1
- package/dist/src/driver/bundler/deferred.d.ts +63 -0
- package/dist/src/driver/bundler/deferred.js +279 -0
- package/dist/src/driver/bundler/deferred.js.map +1 -0
- package/dist/src/driver/bundler/merge.js +214 -2
- package/dist/src/driver/bundler/merge.js.map +1 -1
- package/dist/src/driver/runtime-objects.d.ts +1 -1
- package/dist/src/driver/runtime-objects.js +1 -0
- package/dist/src/driver/runtime-objects.js.map +1 -1
- package/dist/src/driver/version.d.ts +1 -1
- package/dist/src/driver/version.js +1 -1
- package/dist/src/extensions/gui/html.js +89 -13
- package/dist/src/extensions/gui/html.js.map +1 -1
- package/dist/src/extensions/gui/index.js +1 -0
- package/dist/src/extensions/gui/index.js.map +1 -1
- package/dist/src/extensions/gui/three.d.ts +4 -3
- package/dist/src/extensions/gui/three.js +4 -3
- package/dist/src/extensions/gui/three.js.map +1 -1
- package/dist/tests/e2e/gui-script-import-order.test.d.ts +10 -0
- package/dist/tests/e2e/gui-script-import-order.test.js +161 -0
- package/dist/tests/e2e/gui-script-import-order.test.js.map +1 -0
- package/dist/tests/e2e/gui-webgl2-shadow.test.d.ts +10 -0
- package/dist/tests/e2e/gui-webgl2-shadow.test.js +108 -0
- package/dist/tests/e2e/gui-webgl2-shadow.test.js.map +1 -0
- package/dist/tests/e2e/url.test.d.ts +6 -0
- package/dist/tests/e2e/url.test.js +114 -0
- package/dist/tests/e2e/url.test.js.map +1 -0
- package/dist/tests/extensions/gui.test.js +22 -10
- package/dist/tests/extensions/gui.test.js.map +1 -1
- package/doc/gui/webgl.md +20 -10
- package/doc/gui-scripts.md +19 -19
- package/doc/gui.md +10 -8
- package/doc/implemented/fetch.md +5 -0
- package/doc/implemented/modules.md +13 -0
- package/doc/implemented/stdlib.md +17 -0
- package/doc/unimplemented/deviations.md +1 -0
- package/doc/zh-CN/gui/webgl.md +3 -3
- package/doc/zh-CN/gui-scripts.md +14 -13
- package/doc/zh-CN/gui.md +7 -6
- package/doc/zh-CN/implemented/fetch.md +4 -0
- package/doc/zh-CN/implemented/modules.md +10 -0
- package/doc/zh-CN/implemented/stdlib.md +15 -0
- package/doc/zh-CN/unimplemented/deviations.md +1 -0
- package/package.json +7 -7
- package/runtime/ext_gui/dom_api_webgl.cpp +10 -0
- package/runtime/ext_gui/dom_api_webgl2.cpp +33 -1
- package/runtime/ext_gui/gui_engine.h +2 -0
- package/runtime/ext_gui/script.cpp +31 -0
- package/runtime/ext_gui/webgl_glsl.h +3 -0
- package/runtime/ext_gui/webgl_glsl_unroll.cpp +283 -0
- package/runtime/ext_gui/webgl_msl.cpp +4 -0
- package/runtime/ext_gui/webgl_shader.cpp +12 -6
- package/runtime/rt.h +3 -0
- package/runtime/xt_fetch/async.inc +60 -0
- package/runtime/xt_url_object.c +463 -0
- package/src/codegen/generator/state.ts +1 -0
- package/src/codegen/generator/tables.ts +1 -0
- package/src/driver/bundler/deferred.ts +351 -0
- package/src/driver/bundler/merge.ts +219 -2
- package/src/driver/runtime-objects.ts +1 -0
- package/src/driver/version.ts +1 -1
- package/src/extensions/gui/html.ts +91 -15
- package/src/extensions/gui/index.ts +1 -0
- package/src/extensions/gui/three.ts +4 -3
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — `#pragma unroll_loop_start` expansion.
|
|
3
|
+
*
|
|
4
|
+
* three.js marks its material light loops with
|
|
5
|
+
*
|
|
6
|
+
* #pragma unroll_loop_start
|
|
7
|
+
* for ( int i = 0; i < N; i ++ ) { ... UNROLLED_LOOP_INDEX ... [ i ] ... }
|
|
8
|
+
* #pragma unroll_loop_end
|
|
9
|
+
*
|
|
10
|
+
* and expands them in JavaScript before upload. On this runtime the expansion
|
|
11
|
+
* is best done here: the loop body indexes sampler arrays (`shadowMap[ i ]`),
|
|
12
|
+
* and Vulkan GLSL only accepts a *constant* sampler-array index. Expanding the
|
|
13
|
+
* pragma turns every `i` (and `UNROLLED_LOOP_INDEX`) into a literal, so the
|
|
14
|
+
* shadow-map chunks compile without relying on the regex engine or on three.js
|
|
15
|
+
* having unrolled them first. WebGL ignores the unknown pragmas either way.
|
|
16
|
+
*
|
|
17
|
+
* The transformation is deliberately small: only the `for (int i = A; i < B;
|
|
18
|
+
* i ++)` shape three.js emits is recognised. Anything else is left untouched.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
#include "webgl_glsl.h"
|
|
22
|
+
|
|
23
|
+
#include <cctype>
|
|
24
|
+
#include <cstdlib>
|
|
25
|
+
#include <string>
|
|
26
|
+
#include <vector>
|
|
27
|
+
|
|
28
|
+
namespace xtgui {
|
|
29
|
+
namespace {
|
|
30
|
+
|
|
31
|
+
const char *const kStartPragma = "#pragma unroll_loop_start";
|
|
32
|
+
const char *const kEndPragma = "#pragma unroll_loop_end";
|
|
33
|
+
|
|
34
|
+
/** Skip whitespace and comments starting at `at`. */
|
|
35
|
+
size_t skip_space(const std::string &text, size_t at) {
|
|
36
|
+
while (at < text.size()) {
|
|
37
|
+
const char c = text[at];
|
|
38
|
+
if (std::isspace((unsigned char)c) != 0) {
|
|
39
|
+
at++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (c == '/' && at + 1 < text.size() && text[at + 1] == '/') {
|
|
43
|
+
at += 2;
|
|
44
|
+
while (at < text.size() && text[at] != '\n') at++;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (c == '/' && at + 1 < text.size() && text[at + 1] == '*') {
|
|
48
|
+
at += 2;
|
|
49
|
+
while (at + 1 < text.size() && !(text[at] == '*' && text[at + 1] == '/')) at++;
|
|
50
|
+
if (at + 1 < text.size()) at += 2;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
return at;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Index just past the end of a `//` / `#` line starting at `at`. */
|
|
59
|
+
size_t line_end(const std::string &text, size_t at) {
|
|
60
|
+
while (at < text.size() && text[at] != '\n') at++;
|
|
61
|
+
return at;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Match the `}` for the `{` at `open`, skipping comments. */
|
|
65
|
+
size_t matching_brace(const std::string &text, size_t open, size_t *out) {
|
|
66
|
+
int depth = 0;
|
|
67
|
+
for (size_t i = open; i < text.size(); i++) {
|
|
68
|
+
const char c = text[i];
|
|
69
|
+
if (c == '/' && i + 1 < text.size() && text[i + 1] == '/') {
|
|
70
|
+
i += 2;
|
|
71
|
+
while (i < text.size() && text[i] != '\n') i++;
|
|
72
|
+
if (i < text.size()) continue;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (c == '/' && i + 1 < text.size() && text[i + 1] == '*') {
|
|
76
|
+
i += 2;
|
|
77
|
+
while (i + 1 < text.size() && !(text[i] == '*' && text[i + 1] == '/')) i++;
|
|
78
|
+
if (i + 1 < text.size()) i++;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (c == '{') depth++;
|
|
82
|
+
if (c == '}') {
|
|
83
|
+
depth--;
|
|
84
|
+
if (depth == 0) {
|
|
85
|
+
*out = i;
|
|
86
|
+
return i;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return std::string::npos;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
struct LoopHeader {
|
|
94
|
+
std::string variable;
|
|
95
|
+
long start = 0;
|
|
96
|
+
long end = 0;
|
|
97
|
+
bool inclusive = false;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Tokenise a for-header into identifiers, numbers and punctuation. */
|
|
101
|
+
std::vector<std::string> tokens(const std::string &text) {
|
|
102
|
+
std::vector<std::string> out;
|
|
103
|
+
size_t i = 0;
|
|
104
|
+
while (i < text.size()) {
|
|
105
|
+
const char c = text[i];
|
|
106
|
+
if (std::isspace((unsigned char)c) != 0) {
|
|
107
|
+
i++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (std::isalnum((unsigned char)c) != 0 || c == '_') {
|
|
111
|
+
size_t end = i + 1;
|
|
112
|
+
while (end < text.size() &&
|
|
113
|
+
(std::isalnum((unsigned char)text[end]) != 0 || text[end] == '_')) {
|
|
114
|
+
end++;
|
|
115
|
+
}
|
|
116
|
+
out.push_back(text.substr(i, end - i));
|
|
117
|
+
i = end;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if ((c == '+' && i + 1 < text.size() && text[i + 1] == '+') ||
|
|
121
|
+
(c == '-' && i + 1 < text.size() && text[i + 1] == '-')) {
|
|
122
|
+
out.push_back(text.substr(i, 2));
|
|
123
|
+
i += 2;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if ((c == '+' || c == '-' || c == '<' || c == '=' || c == '!') && i + 1 < text.size() &&
|
|
127
|
+
text[i + 1] == '=') {
|
|
128
|
+
out.push_back(text.substr(i, 2));
|
|
129
|
+
i += 2;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
out.push_back(std::string(1, c));
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
bool parse_header(const std::string &header, LoopHeader *out) {
|
|
139
|
+
std::vector<std::string> parts = tokens(header);
|
|
140
|
+
size_t at = 0;
|
|
141
|
+
/* Optional qualifiers (`int`, `uint`, `highp int`, …). */
|
|
142
|
+
while (at < parts.size() && (parts[at] == "int" || parts[at] == "uint" || parts[at] == "highp" ||
|
|
143
|
+
parts[at] == "mediump" || parts[at] == "lowp")) {
|
|
144
|
+
at++;
|
|
145
|
+
}
|
|
146
|
+
if (at >= parts.size()) return false;
|
|
147
|
+
out->variable = parts[at++];
|
|
148
|
+
if (at >= parts.size() || parts[at] != "=") return false;
|
|
149
|
+
at++;
|
|
150
|
+
if (at >= parts.size()) return false;
|
|
151
|
+
char *end = nullptr;
|
|
152
|
+
out->start = std::strtol(parts[at].c_str(), &end, 10);
|
|
153
|
+
if (end == nullptr || *end != 0) return false;
|
|
154
|
+
at++;
|
|
155
|
+
if (at >= parts.size() || parts[at] != ";") return false;
|
|
156
|
+
at++;
|
|
157
|
+
/* `i < N` / `i <= N`; the repeated variable may be spelled differently. */
|
|
158
|
+
if (at >= parts.size()) return false;
|
|
159
|
+
at++;
|
|
160
|
+
if (at >= parts.size()) return false;
|
|
161
|
+
out->inclusive = parts[at] == "<=";
|
|
162
|
+
if (!out->inclusive && parts[at] != "<") return false;
|
|
163
|
+
at++;
|
|
164
|
+
if (at >= parts.size()) return false;
|
|
165
|
+
out->end = std::strtol(parts[at].c_str(), &end, 10);
|
|
166
|
+
if (end == nullptr || *end != 0) return false;
|
|
167
|
+
at++;
|
|
168
|
+
if (at >= parts.size() || parts[at] != ";") return false;
|
|
169
|
+
at++;
|
|
170
|
+
if (at >= parts.size()) return false;
|
|
171
|
+
at++;
|
|
172
|
+
if (at >= parts.size() || parts[at] != "++") return false;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Replace whole-word `name` with `value`. */
|
|
177
|
+
std::string substitute(const std::string &body, const std::string &name, long value) {
|
|
178
|
+
const std::string text = std::to_string(value);
|
|
179
|
+
std::string out;
|
|
180
|
+
out.reserve(body.size());
|
|
181
|
+
size_t i = 0;
|
|
182
|
+
while (i < body.size()) {
|
|
183
|
+
const char c = body[i];
|
|
184
|
+
if (std::isalpha((unsigned char)c) == 0 && c != '_') {
|
|
185
|
+
out += c;
|
|
186
|
+
i++;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
size_t end = i + 1;
|
|
190
|
+
while (end < body.size() &&
|
|
191
|
+
(std::isalnum((unsigned char)body[end]) != 0 || body[end] == '_')) {
|
|
192
|
+
end++;
|
|
193
|
+
}
|
|
194
|
+
const std::string word = body.substr(i, end - i);
|
|
195
|
+
if (word == name || word == "UNROLLED_LOOP_INDEX") {
|
|
196
|
+
out += text;
|
|
197
|
+
} else {
|
|
198
|
+
out += word;
|
|
199
|
+
}
|
|
200
|
+
i = end;
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
} // namespace
|
|
206
|
+
|
|
207
|
+
std::string unroll_glsl_pragmas(const std::string &source) {
|
|
208
|
+
if (source.find(kStartPragma) == std::string::npos) return source;
|
|
209
|
+
std::string result;
|
|
210
|
+
size_t cursor = 0;
|
|
211
|
+
while (cursor < source.size()) {
|
|
212
|
+
const size_t pragma = source.find(kStartPragma, cursor);
|
|
213
|
+
if (pragma == std::string::npos) {
|
|
214
|
+
result.append(source, cursor, std::string::npos);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
result.append(source, cursor, pragma - cursor);
|
|
218
|
+
size_t at = line_end(source, pragma + std::string(kStartPragma).size());
|
|
219
|
+
const size_t after_header = skip_space(source, at);
|
|
220
|
+
if (source.compare(after_header, 3, "for") != 0) {
|
|
221
|
+
/* Not the shape three.js emits: keep the pragma and move on. */
|
|
222
|
+
result.append(source, pragma, at - pragma);
|
|
223
|
+
cursor = at;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
size_t open_paren = skip_space(source, after_header + 3);
|
|
227
|
+
if (open_paren >= source.size() || source[open_paren] != '(') {
|
|
228
|
+
result.append(source, pragma, at - pragma);
|
|
229
|
+
cursor = at;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
int depth = 0;
|
|
233
|
+
size_t close_paren = open_paren;
|
|
234
|
+
for (; close_paren < source.size(); close_paren++) {
|
|
235
|
+
if (source[close_paren] == '(') depth++;
|
|
236
|
+
if (source[close_paren] == ')') {
|
|
237
|
+
depth--;
|
|
238
|
+
if (depth == 0) break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (close_paren >= source.size()) {
|
|
242
|
+
result.append(source, pragma, at - pragma);
|
|
243
|
+
cursor = at;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
LoopHeader loop;
|
|
247
|
+
if (!parse_header(source.substr(open_paren + 1, close_paren - open_paren - 1), &loop) ||
|
|
248
|
+
loop.end < loop.start || loop.end - loop.start > 4096) {
|
|
249
|
+
result.append(source, pragma, at - pragma);
|
|
250
|
+
cursor = at;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
size_t open_brace = skip_space(source, close_paren + 1);
|
|
254
|
+
if (open_brace >= source.size() || source[open_brace] != '{') {
|
|
255
|
+
result.append(source, pragma, at - pragma);
|
|
256
|
+
cursor = at;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
size_t close_brace = 0;
|
|
260
|
+
if (matching_brace(source, open_brace, &close_brace) == std::string::npos) {
|
|
261
|
+
result.append(source, pragma, at - pragma);
|
|
262
|
+
cursor = at;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
size_t end = skip_space(source, close_brace + 1);
|
|
266
|
+
if (source.compare(end, std::string(kEndPragma).size(), kEndPragma) != 0) {
|
|
267
|
+
result.append(source, pragma, at - pragma);
|
|
268
|
+
cursor = at;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
end = line_end(source, end + std::string(kEndPragma).size());
|
|
272
|
+
const std::string body = source.substr(open_brace + 1, close_brace - open_brace - 1);
|
|
273
|
+
const long last = loop.inclusive ? loop.end : loop.end - 1;
|
|
274
|
+
for (long i = loop.start; i <= last; i++) {
|
|
275
|
+
result += substitute(body, loop.variable, i);
|
|
276
|
+
result += '\n';
|
|
277
|
+
}
|
|
278
|
+
cursor = end;
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
} // namespace xtgui
|
|
@@ -36,6 +36,10 @@ bool cross_compile_msl(const std::vector<uint32_t> &spirv, bool fragment,
|
|
|
36
36
|
* 0..num_samplers-1, so any other allocation would sample the wrong
|
|
37
37
|
* texture (or none at all). */
|
|
38
38
|
options.enable_decoration_binding = true;
|
|
39
|
+
/* Sampler arrays (three.js shadow maps) need MSL 2.0: with the default
|
|
40
|
+
* 1.2 target SPIRV-Cross rejects "MSL 2.0 or greater is required for
|
|
41
|
+
* arrays of textures". Every Metal device SDL_GPU runs on supports 2.0. */
|
|
42
|
+
options.msl_version = spirv_cross::CompilerMSL::Options::make_msl_version(2, 0);
|
|
39
43
|
compiler.set_msl_options(options);
|
|
40
44
|
*source = compiler.compile();
|
|
41
45
|
*entry = compiler.get_cleansed_entry_point_name(
|
|
@@ -174,8 +174,12 @@ bool WebGLContext::compile_shader(uint32_t id) {
|
|
|
174
174
|
WebGLShader *record = shader(id);
|
|
175
175
|
if (record == nullptr) return false;
|
|
176
176
|
const bool fragment = record->type == GL_FRAGMENT_SHADER;
|
|
177
|
+
/* Expand three.js's pragma-marked light loops first: a sampler-array index
|
|
178
|
+
* must be constant in Vulkan GLSL, and the expansion is the only place that
|
|
179
|
+
* knows the loop bounds. Scanning and rewriting then share one text. */
|
|
180
|
+
const std::string prepared = unroll_glsl_pragmas(record->source);
|
|
177
181
|
GlslShaderInfo info;
|
|
178
|
-
scan_glsl(
|
|
182
|
+
scan_glsl(prepared, fragment, &info);
|
|
179
183
|
if (!info.unsupported.empty()) {
|
|
180
184
|
record->log = info.unsupported;
|
|
181
185
|
record->compiled = false;
|
|
@@ -186,7 +190,7 @@ bool WebGLContext::compile_shader(uint32_t id) {
|
|
|
186
190
|
assign_locations(info.varyings, {}, &layout.varyings);
|
|
187
191
|
assign_locations(info.samplers, {}, &layout.samplers);
|
|
188
192
|
layout.block_members = info.uniforms;
|
|
189
|
-
const std::string source = rewrite_glsl(
|
|
193
|
+
const std::string source = rewrite_glsl(prepared, fragment, layout, info);
|
|
190
194
|
std::vector<uint32_t> spirv;
|
|
191
195
|
record->compiled = compile_glsl(source, fragment, &spirv, &record->log);
|
|
192
196
|
return record->compiled;
|
|
@@ -289,10 +293,12 @@ bool WebGLContext::link_program(uint32_t id) {
|
|
|
289
293
|
return false;
|
|
290
294
|
}
|
|
291
295
|
|
|
296
|
+
const std::string prepared_vertex = unroll_glsl_pragmas(vertex->source);
|
|
297
|
+
const std::string prepared_fragment = unroll_glsl_pragmas(fragment->source);
|
|
292
298
|
GlslShaderInfo vs_info;
|
|
293
299
|
GlslShaderInfo fs_info;
|
|
294
|
-
scan_glsl(
|
|
295
|
-
scan_glsl(
|
|
300
|
+
scan_glsl(prepared_vertex, false, &vs_info);
|
|
301
|
+
scan_glsl(prepared_fragment, true, &fs_info);
|
|
296
302
|
if (!vs_info.unsupported.empty() || !fs_info.unsupported.empty()) {
|
|
297
303
|
record->log = vs_info.unsupported.empty() ? fs_info.unsupported : vs_info.unsupported;
|
|
298
304
|
record->linked = false;
|
|
@@ -361,8 +367,8 @@ bool WebGLContext::link_program(uint32_t id) {
|
|
|
361
367
|
}
|
|
362
368
|
fs_layout.varyings = vs_layout.varyings;
|
|
363
369
|
|
|
364
|
-
const std::string vs_source = rewrite_glsl(
|
|
365
|
-
const std::string fs_source = rewrite_glsl(
|
|
370
|
+
const std::string vs_source = rewrite_glsl(prepared_vertex, false, vs_layout, vs_info);
|
|
371
|
+
const std::string fs_source = rewrite_glsl(prepared_fragment, true, fs_layout, fs_info);
|
|
366
372
|
std::vector<uint32_t> vs_spirv;
|
|
367
373
|
std::vector<uint32_t> fs_spirv;
|
|
368
374
|
std::string log;
|
package/runtime/rt.h
CHANGED
|
@@ -305,6 +305,9 @@ xt_value xt_date_static(xt_value name, int32_t argc, xt_value *argv);
|
|
|
305
305
|
xt_value xt_regexp_ctor(int32_t argc, xt_value *argv);
|
|
306
306
|
|
|
307
307
|
/* -- node:url ------------------------------------------------------------- */
|
|
308
|
+
xt_value xt_url_ctor(int32_t argc, xt_value *argv);
|
|
309
|
+
/** Resolve a `fetch` reference to a filesystem path (file: URLs and relative paths). */
|
|
310
|
+
int xt_url_file_path(const char *url, char *out, size_t size);
|
|
308
311
|
xt_value xt_url_path_to_file_url(int32_t argc, xt_value *argv);
|
|
309
312
|
xt_value xt_url_file_url_to_path(int32_t argc, xt_value *argv);
|
|
310
313
|
|
|
@@ -201,6 +201,66 @@ static void xt_fetch_hop_complete(xt_async_job *job) {
|
|
|
201
201
|
|
|
202
202
|
/* Main thread: validate the current URL and queue one exchange. */
|
|
203
203
|
static void xt_fetch_start(xt_fetch_state *state) {
|
|
204
|
+
/* `file:` URLs and references with no scheme (a relative path) read a local
|
|
205
|
+
* file. This is what lets a compiled page load an asset from disk without an
|
|
206
|
+
* HTTP server; the response is built on this thread because a local read is
|
|
207
|
+
* already complete. */
|
|
208
|
+
char path[4096];
|
|
209
|
+
if (xt_url_file_path(state->url, path, sizeof(path))) {
|
|
210
|
+
if (strcmp(state->method, "GET") != 0 && strcmp(state->method, "HEAD") != 0) {
|
|
211
|
+
xt_fetch_state_reject(state, "fetch failed: file URLs only support GET or HEAD");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
FILE *file = fopen(path, "rb");
|
|
215
|
+
if (!file) {
|
|
216
|
+
xt_fetch_state_reject(state, "fetch failed: cannot read file");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
size_t capacity = 0;
|
|
220
|
+
size_t length = 0;
|
|
221
|
+
char *body = NULL;
|
|
222
|
+
char chunk[16384];
|
|
223
|
+
for (;;) {
|
|
224
|
+
size_t count = fread(chunk, 1, sizeof(chunk), file);
|
|
225
|
+
if (count > 0) {
|
|
226
|
+
if (length + count + 1 > capacity) {
|
|
227
|
+
capacity = (length + count + 1) * 2;
|
|
228
|
+
char *grown = (char *)realloc(body, capacity);
|
|
229
|
+
if (!grown) {
|
|
230
|
+
free(body);
|
|
231
|
+
fclose(file);
|
|
232
|
+
xt_fetch_state_reject(state, "fetch failed");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
body = grown;
|
|
236
|
+
}
|
|
237
|
+
memcpy(body + length, chunk, count);
|
|
238
|
+
length += count;
|
|
239
|
+
}
|
|
240
|
+
if (count < sizeof(chunk)) break;
|
|
241
|
+
}
|
|
242
|
+
fclose(file);
|
|
243
|
+
|
|
244
|
+
char href[4300];
|
|
245
|
+
if (path[0] == '/') {
|
|
246
|
+
snprintf(href, sizeof(href), "file://%s", path);
|
|
247
|
+
} else {
|
|
248
|
+
snprintf(href, sizeof(href), "file:///%s", path);
|
|
249
|
+
}
|
|
250
|
+
xt_value headers = xt_headers_new();
|
|
251
|
+
char lengthText[32];
|
|
252
|
+
snprintf(lengthText, sizeof(lengthText), "%zu", length);
|
|
253
|
+
xt_headers_set_raw(headers, "content-length", lengthText);
|
|
254
|
+
xt_headers_set_raw(headers, "content-type", "application/octet-stream");
|
|
255
|
+
size_t bodyLength = strcmp(state->method, "HEAD") == 0 ? 0 : length;
|
|
256
|
+
xt_value response = xt_response_new(headers, 200, "OK", href, body, bodyLength);
|
|
257
|
+
free(body);
|
|
258
|
+
xt_value promise = state->promise;
|
|
259
|
+
xt_fetch_state_free(state);
|
|
260
|
+
xt_promise_fulfill(promise, response);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
204
264
|
xt_fetch_url parsed;
|
|
205
265
|
if (xt_fetch_parse_url(state->url, &parsed) != 0) {
|
|
206
266
|
xt_fetch_state_reject(state, "Failed to parse URL");
|