mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
Compare commits
43 Commits
main
...
fetch_lazy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa0d5ba12 | ||
|
|
4d1e416299 | ||
|
|
3badcdbdbd | ||
|
|
fcd82b2c14 | ||
|
|
d0621510cc | ||
|
|
2a7a8bc2a6 | ||
|
|
af916dea1d | ||
|
|
31335fc4fb | ||
|
|
c84634093d | ||
|
|
37d8d2642d | ||
|
|
0423a178e9 | ||
|
|
7acf67d668 | ||
|
|
ef1fece40c | ||
|
|
ebb590250f | ||
|
|
03130a95d8 | ||
|
|
e133717f7f | ||
|
|
968c695da1 | ||
|
|
707116a030 | ||
|
|
01966f41ff | ||
|
|
141d17dd55 | ||
|
|
a3c2daf306 | ||
|
|
dc60fac90d | ||
|
|
a5e2e8ea15 | ||
|
|
8295c2abe5 | ||
|
|
5997be89f6 | ||
|
|
1c89cfe5d4 | ||
|
|
b5021bd9fa | ||
|
|
4fd365b520 | ||
|
|
479cd5ab1a | ||
|
|
8285cbcaa9 | ||
|
|
545d97b5c0 | ||
|
|
11016abdd3 | ||
|
|
066df87dd4 | ||
|
|
91899912d8 | ||
|
|
4ceca6b90b | ||
|
|
ec936417c6 | ||
|
|
4b75b33eb3 | ||
|
|
1d7e731034 | ||
|
|
ab60f64452 | ||
|
|
9757ea7b0f | ||
|
|
855583874f | ||
|
|
9efc27c2bb | ||
|
|
cab5117d85 |
@@ -36,6 +36,8 @@ const WebApis = struct {
|
|||||||
@import("xhr/form_data.zig").Interfaces,
|
@import("xhr/form_data.zig").Interfaces,
|
||||||
@import("xhr/File.zig"),
|
@import("xhr/File.zig"),
|
||||||
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
||||||
|
@import("fetch/fetch.zig").Interfaces,
|
||||||
|
@import("streams/streams.zig").Interfaces,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
227
src/browser/fetch/Headers.zig
Normal file
227
src/browser/fetch/Headers.zig
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
const URL = @import("../../url.zig").URL;
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
const iterator = @import("../iterator/iterator.zig");
|
||||||
|
|
||||||
|
const v8 = @import("v8");
|
||||||
|
const Env = @import("../env.zig").Env;
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
||||||
|
const Headers = @This();
|
||||||
|
|
||||||
|
// Case-Insensitive String HashMap.
|
||||||
|
// This allows us to avoid having to allocate lowercase keys all the time.
|
||||||
|
const HeaderHashMap = std.HashMapUnmanaged([]const u8, []const u8, struct {
|
||||||
|
pub fn hash(_: @This(), s: []const u8) u64 {
|
||||||
|
var buf: [64]u8 = undefined;
|
||||||
|
var hasher = std.hash.Wyhash.init(s.len);
|
||||||
|
|
||||||
|
var key = s;
|
||||||
|
while (key.len >= 64) {
|
||||||
|
const lower = std.ascii.lowerString(buf[0..], key[0..64]);
|
||||||
|
hasher.update(lower);
|
||||||
|
key = key[64..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.len > 0) {
|
||||||
|
const lower = std.ascii.lowerString(buf[0..key.len], key);
|
||||||
|
hasher.update(lower);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasher.final();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(_: @This(), a: []const u8, b: []const u8) bool {
|
||||||
|
return std.ascii.eqlIgnoreCase(a, b);
|
||||||
|
}
|
||||||
|
}, 80);
|
||||||
|
|
||||||
|
headers: HeaderHashMap = .empty,
|
||||||
|
|
||||||
|
// They can either be:
|
||||||
|
//
|
||||||
|
// 1. An array of string pairs.
|
||||||
|
// 2. An object with string keys to string values.
|
||||||
|
// 3. Another Headers object.
|
||||||
|
pub const HeadersInit = union(enum) {
|
||||||
|
// List of Pairs of []const u8
|
||||||
|
strings: []const [2][]const u8,
|
||||||
|
// Headers
|
||||||
|
headers: *Headers,
|
||||||
|
// Mappings
|
||||||
|
object: Env.JsObject,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
|
||||||
|
const arena = page.arena;
|
||||||
|
var headers: HeaderHashMap = .empty;
|
||||||
|
|
||||||
|
if (_init) |init| {
|
||||||
|
switch (init) {
|
||||||
|
.strings => |kvs| {
|
||||||
|
for (kvs) |pair| {
|
||||||
|
const key = try arena.dupe(u8, pair[0]);
|
||||||
|
const value = try arena.dupe(u8, pair[1]);
|
||||||
|
|
||||||
|
try headers.put(arena, key, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.headers => |hdrs| {
|
||||||
|
var iter = hdrs.headers.iterator();
|
||||||
|
while (iter.next()) |entry| {
|
||||||
|
try headers.put(arena, entry.key_ptr.*, entry.value_ptr.*);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.object => |obj| {
|
||||||
|
var iter = obj.nameIterator();
|
||||||
|
while (try iter.next()) |name_value| {
|
||||||
|
const name = try name_value.toString(arena);
|
||||||
|
const value = try obj.get(name);
|
||||||
|
const value_string = try value.toString(arena);
|
||||||
|
|
||||||
|
try headers.put(arena, name, value_string);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.headers = headers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append(self: *Headers, name: []const u8, value: []const u8, allocator: std.mem.Allocator) !void {
|
||||||
|
const key = try allocator.dupe(u8, name);
|
||||||
|
const gop = try self.headers.getOrPut(allocator, key);
|
||||||
|
|
||||||
|
if (gop.found_existing) {
|
||||||
|
// If we found it, append the value.
|
||||||
|
const new_value = try std.fmt.allocPrint(allocator, "{s}, {s}", .{ gop.value_ptr.*, value });
|
||||||
|
gop.value_ptr.* = new_value;
|
||||||
|
} else {
|
||||||
|
// Otherwise, we should just put it in.
|
||||||
|
gop.value_ptr.* = try allocator.dupe(u8, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
|
||||||
|
const arena = page.arena;
|
||||||
|
try self.append(name, value, arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _delete(self: *Headers, name: []const u8) void {
|
||||||
|
_ = self.headers.remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const HeadersEntryIterator = struct {
|
||||||
|
slot: [2][]const u8,
|
||||||
|
iter: HeaderHashMap.Iterator,
|
||||||
|
|
||||||
|
// TODO: these SHOULD be in lexigraphical order but I'm not sure how actually
|
||||||
|
// important that is.
|
||||||
|
pub fn _next(self: *HeadersEntryIterator) ?[2][]const u8 {
|
||||||
|
if (self.iter.next()) |entry| {
|
||||||
|
self.slot[0] = entry.key_ptr.*;
|
||||||
|
self.slot[1] = entry.value_ptr.*;
|
||||||
|
return self.slot;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn _entries(self: *const Headers) HeadersEntryIterable {
|
||||||
|
return .{
|
||||||
|
.inner = .{
|
||||||
|
.slot = undefined,
|
||||||
|
.iter = self.headers.iterator(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _forEach(self: *Headers, callback_fn: Env.Function, this_arg: ?Env.JsObject) !void {
|
||||||
|
var iter = self.headers.iterator();
|
||||||
|
|
||||||
|
const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn;
|
||||||
|
|
||||||
|
while (iter.next()) |entry| {
|
||||||
|
try cb.call(void, .{ entry.key_ptr.*, entry.value_ptr.*, self });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _get(self: *const Headers, name: []const u8) ?[]const u8 {
|
||||||
|
return self.headers.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _has(self: *const Headers, name: []const u8) bool {
|
||||||
|
return self.headers.contains(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const HeadersKeyIterator = struct {
|
||||||
|
iter: HeaderHashMap.KeyIterator,
|
||||||
|
|
||||||
|
pub fn _next(self: *HeadersKeyIterator) ?[]const u8 {
|
||||||
|
if (self.iter.next()) |key| {
|
||||||
|
return key.*;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn _keys(self: *const Headers) HeadersKeyIterable {
|
||||||
|
return .{ .inner = .{ .iter = self.headers.keyIterator() } };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
|
||||||
|
const arena = page.arena;
|
||||||
|
|
||||||
|
const key = try arena.dupe(u8, name);
|
||||||
|
const gop = try self.headers.getOrPut(arena, key);
|
||||||
|
gop.value_ptr.* = try arena.dupe(u8, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const HeadersValueIterator = struct {
|
||||||
|
iter: HeaderHashMap.ValueIterator,
|
||||||
|
|
||||||
|
pub fn _next(self: *HeadersValueIterator) ?[]const u8 {
|
||||||
|
if (self.iter.next()) |value| {
|
||||||
|
return value.*;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn _values(self: *const Headers) HeadersValueIterable {
|
||||||
|
return .{ .inner = .{ .iter = self.headers.valueIterator() } };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const HeadersKeyIterable = iterator.Iterable(HeadersKeyIterator, "HeadersKeyIterator");
|
||||||
|
pub const HeadersValueIterable = iterator.Iterable(HeadersValueIterator, "HeadersValueIterator");
|
||||||
|
pub const HeadersEntryIterable = iterator.Iterable(HeadersEntryIterator, "HeadersEntryIterator");
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "fetch: Headers" {
|
||||||
|
try testing.htmlRunner("fetch/headers.html");
|
||||||
|
}
|
||||||
266
src/browser/fetch/Request.zig
Normal file
266
src/browser/fetch/Request.zig
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const URL = @import("../../url.zig").URL;
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
const Response = @import("./Response.zig");
|
||||||
|
const Http = @import("../../http/Http.zig");
|
||||||
|
const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||||
|
|
||||||
|
const v8 = @import("v8");
|
||||||
|
const Env = @import("../env.zig").Env;
|
||||||
|
|
||||||
|
const Headers = @import("Headers.zig");
|
||||||
|
const HeadersInit = @import("Headers.zig").HeadersInit;
|
||||||
|
|
||||||
|
pub const RequestInput = union(enum) {
|
||||||
|
string: []const u8,
|
||||||
|
request: *Request,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestCache = enum {
|
||||||
|
default,
|
||||||
|
@"no-store",
|
||||||
|
reload,
|
||||||
|
@"no-cache",
|
||||||
|
@"force-cache",
|
||||||
|
@"only-if-cached",
|
||||||
|
|
||||||
|
pub fn fromString(str: []const u8) ?RequestCache {
|
||||||
|
for (std.enums.values(RequestCache)) |cache| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toString(self: RequestCache) []const u8 {
|
||||||
|
return @tagName(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RequestCredentials = enum {
|
||||||
|
omit,
|
||||||
|
@"same-origin",
|
||||||
|
include,
|
||||||
|
|
||||||
|
pub fn fromString(str: []const u8) ?RequestCredentials {
|
||||||
|
for (std.enums.values(RequestCredentials)) |cache| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toString(self: RequestCredentials) []const u8 {
|
||||||
|
return @tagName(self);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
|
||||||
|
pub const RequestInit = struct {
|
||||||
|
body: ?[]const u8 = null,
|
||||||
|
cache: ?[]const u8 = null,
|
||||||
|
credentials: ?[]const u8 = null,
|
||||||
|
headers: ?HeadersInit = null,
|
||||||
|
integrity: ?[]const u8 = null,
|
||||||
|
method: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
|
||||||
|
const Request = @This();
|
||||||
|
|
||||||
|
method: Http.Method,
|
||||||
|
url: [:0]const u8,
|
||||||
|
cache: RequestCache,
|
||||||
|
credentials: RequestCredentials,
|
||||||
|
headers: Headers,
|
||||||
|
body: ?[]const u8,
|
||||||
|
body_used: bool = false,
|
||||||
|
integrity: []const u8,
|
||||||
|
|
||||||
|
pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Request {
|
||||||
|
const arena = page.arena;
|
||||||
|
const options: RequestInit = _options orelse .{};
|
||||||
|
|
||||||
|
const url: [:0]const u8 = blk: switch (input) {
|
||||||
|
.string => |str| {
|
||||||
|
break :blk try URL.stitch(arena, str, page.url.raw, .{ .null_terminated = true });
|
||||||
|
},
|
||||||
|
.request => |req| {
|
||||||
|
break :blk try arena.dupeZ(u8, req.url);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const body = if (options.body) |body| try arena.dupe(u8, body) else null;
|
||||||
|
const cache = (if (options.cache) |cache| RequestCache.fromString(cache) else null) orelse RequestCache.default;
|
||||||
|
const credentials = (if (options.credentials) |creds| RequestCredentials.fromString(creds) else null) orelse RequestCredentials.@"same-origin";
|
||||||
|
const integrity = if (options.integrity) |integ| try arena.dupe(u8, integ) else "";
|
||||||
|
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
|
||||||
|
|
||||||
|
const method: Http.Method = blk: {
|
||||||
|
if (options.method) |given_method| {
|
||||||
|
for (std.enums.values(Http.Method)) |method| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(given_method, @tagName(method))) {
|
||||||
|
break :blk method;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break :blk Http.Method.GET;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.method = method,
|
||||||
|
.url = url,
|
||||||
|
.cache = cache,
|
||||||
|
.credentials = credentials,
|
||||||
|
.headers = headers,
|
||||||
|
.body = body,
|
||||||
|
.integrity = integrity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream {
|
||||||
|
if (self.body) |body| {
|
||||||
|
const stream = try ReadableStream.constructor(null, null, page);
|
||||||
|
try stream.queue.append(page.arena, body);
|
||||||
|
return stream;
|
||||||
|
} else return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bodyUsed(self: *const Request) bool {
|
||||||
|
return self.body_used;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cache(self: *const Request) RequestCache {
|
||||||
|
return self.cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_credentials(self: *const Request) RequestCredentials {
|
||||||
|
return self.credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_headers(self: *Request) *Headers {
|
||||||
|
return &self.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_integrity(self: *const Request) []const u8 {
|
||||||
|
return self.integrity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: If we ever support the Navigation API, we need isHistoryNavigation
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Request/isHistoryNavigation
|
||||||
|
|
||||||
|
pub fn get_method(self: *const Request) []const u8 {
|
||||||
|
return @tagName(self.method);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_url(self: *const Request) []const u8 {
|
||||||
|
return self.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _clone(self: *Request) !Request {
|
||||||
|
// Not allowed to clone if the body was used.
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK to just return the same fields BECAUSE
|
||||||
|
// all of these fields are read-only and can't be modified.
|
||||||
|
return Request{
|
||||||
|
.body = self.body,
|
||||||
|
.body_used = self.body_used,
|
||||||
|
.cache = self.cache,
|
||||||
|
.credentials = self.credentials,
|
||||||
|
.headers = self.headers,
|
||||||
|
.method = self.method,
|
||||||
|
.integrity = self.integrity,
|
||||||
|
.url = self.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolver = Env.PromiseResolver{
|
||||||
|
.js_context = page.main_context,
|
||||||
|
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||||
|
};
|
||||||
|
|
||||||
|
try resolver.resolve(self.body);
|
||||||
|
self.body_used = true;
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _json(self: *Response, page: *Page) !Env.Promise {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolver = Env.PromiseResolver{
|
||||||
|
.js_context = page.main_context,
|
||||||
|
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||||
|
};
|
||||||
|
|
||||||
|
const p = std.json.parseFromSliceLeaky(
|
||||||
|
std.json.Value,
|
||||||
|
page.call_arena,
|
||||||
|
self.body,
|
||||||
|
.{},
|
||||||
|
) catch |e| {
|
||||||
|
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" });
|
||||||
|
return error.SyntaxError;
|
||||||
|
};
|
||||||
|
|
||||||
|
try resolver.resolve(p);
|
||||||
|
self.body_used = true;
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _text(self: *Response, page: *Page) !Env.Promise {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolver = Env.PromiseResolver{
|
||||||
|
.js_context = page.main_context,
|
||||||
|
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||||
|
};
|
||||||
|
|
||||||
|
try resolver.resolve(self.body);
|
||||||
|
self.body_used = true;
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "fetch: Request" {
|
||||||
|
try testing.htmlRunner("fetch/request.html");
|
||||||
|
}
|
||||||
196
src/browser/fetch/Response.zig
Normal file
196
src/browser/fetch/Response.zig
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const v8 = @import("v8");
|
||||||
|
|
||||||
|
const HttpClient = @import("../../http/Client.zig");
|
||||||
|
const Http = @import("../../http/Http.zig");
|
||||||
|
const URL = @import("../../url.zig").URL;
|
||||||
|
|
||||||
|
const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||||
|
const Headers = @import("Headers.zig");
|
||||||
|
const HeadersInit = @import("Headers.zig").HeadersInit;
|
||||||
|
|
||||||
|
const Env = @import("../env.zig").Env;
|
||||||
|
const Mime = @import("../mime.zig").Mime;
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Response
|
||||||
|
const Response = @This();
|
||||||
|
|
||||||
|
status: u16 = 200,
|
||||||
|
status_text: []const u8 = "",
|
||||||
|
headers: Headers,
|
||||||
|
mime: ?Mime = null,
|
||||||
|
url: []const u8 = "",
|
||||||
|
body: []const u8 = "",
|
||||||
|
body_used: bool = false,
|
||||||
|
redirected: bool = false,
|
||||||
|
|
||||||
|
const ResponseBody = union(enum) {
|
||||||
|
string: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResponseOptions = struct {
|
||||||
|
status: u16 = 200,
|
||||||
|
statusText: ?[]const u8 = null,
|
||||||
|
headers: ?HeadersInit = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Page) !Response {
|
||||||
|
const arena = page.arena;
|
||||||
|
|
||||||
|
const options: ResponseOptions = _options orelse .{};
|
||||||
|
|
||||||
|
const body = blk: {
|
||||||
|
if (_input) |input| {
|
||||||
|
switch (input) {
|
||||||
|
.string => |str| {
|
||||||
|
break :blk try arena.dupe(u8, str);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break :blk "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{};
|
||||||
|
const status_text = if (options.statusText) |st| try arena.dupe(u8, st) else "";
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.body = body,
|
||||||
|
.headers = headers,
|
||||||
|
.status = options.status,
|
||||||
|
.status_text = status_text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
|
||||||
|
const stream = try ReadableStream.constructor(null, null, page);
|
||||||
|
try stream.queue.append(page.arena, self.body);
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bodyUsed(self: *const Response) bool {
|
||||||
|
return self.body_used;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_headers(self: *Response) *Headers {
|
||||||
|
return &self.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ok(self: *const Response) bool {
|
||||||
|
return self.status >= 200 and self.status <= 299;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_redirected(self: *const Response) bool {
|
||||||
|
return self.redirected;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_status(self: *const Response) u16 {
|
||||||
|
return self.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_statusText(self: *const Response) []const u8 {
|
||||||
|
return self.status_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_url(self: *const Response) []const u8 {
|
||||||
|
return self.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _clone(self: *const Response) !Response {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK to just return the same fields BECAUSE
|
||||||
|
// all of these fields are read-only and can't be modified.
|
||||||
|
return Response{
|
||||||
|
.body = self.body,
|
||||||
|
.body_used = self.body_used,
|
||||||
|
.mime = self.mime,
|
||||||
|
.headers = self.headers,
|
||||||
|
.redirected = self.redirected,
|
||||||
|
.status = self.status,
|
||||||
|
.url = self.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolver = Env.PromiseResolver{
|
||||||
|
.js_context = page.main_context,
|
||||||
|
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||||
|
};
|
||||||
|
|
||||||
|
try resolver.resolve(self.body);
|
||||||
|
self.body_used = true;
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _json(self: *Response, page: *Page) !Env.Promise {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolver = Env.PromiseResolver{
|
||||||
|
.js_context = page.main_context,
|
||||||
|
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||||
|
};
|
||||||
|
|
||||||
|
const p = std.json.parseFromSliceLeaky(
|
||||||
|
std.json.Value,
|
||||||
|
page.call_arena,
|
||||||
|
self.body,
|
||||||
|
.{},
|
||||||
|
) catch |e| {
|
||||||
|
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" });
|
||||||
|
return error.SyntaxError;
|
||||||
|
};
|
||||||
|
|
||||||
|
try resolver.resolve(p);
|
||||||
|
self.body_used = true;
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _text(self: *Response, page: *Page) !Env.Promise {
|
||||||
|
if (self.body_used) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolver = Env.PromiseResolver{
|
||||||
|
.js_context = page.main_context,
|
||||||
|
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
|
||||||
|
};
|
||||||
|
|
||||||
|
try resolver.resolve(self.body);
|
||||||
|
self.body_used = true;
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "fetch: Response" {
|
||||||
|
try testing.htmlRunner("fetch/response.html");
|
||||||
|
}
|
||||||
212
src/browser/fetch/fetch.zig
Normal file
212
src/browser/fetch/fetch.zig
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const Env = @import("../env.zig").Env;
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
|
const Http = @import("../../http/Http.zig");
|
||||||
|
const HttpClient = @import("../../http/Client.zig");
|
||||||
|
const Mime = @import("../mime.zig").Mime;
|
||||||
|
|
||||||
|
const Headers = @import("Headers.zig");
|
||||||
|
|
||||||
|
const RequestInput = @import("Request.zig").RequestInput;
|
||||||
|
const RequestInit = @import("Request.zig").RequestInit;
|
||||||
|
const Request = @import("Request.zig");
|
||||||
|
const Response = @import("Response.zig");
|
||||||
|
|
||||||
|
pub const Interfaces = .{
|
||||||
|
@import("Headers.zig"),
|
||||||
|
@import("Headers.zig").HeadersEntryIterable,
|
||||||
|
@import("Headers.zig").HeadersKeyIterable,
|
||||||
|
@import("Headers.zig").HeadersValueIterable,
|
||||||
|
@import("Request.zig"),
|
||||||
|
@import("Response.zig"),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const FetchContext = struct {
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
js_ctx: *Env.JsContext,
|
||||||
|
promise_resolver: Env.PersistentPromiseResolver,
|
||||||
|
|
||||||
|
method: Http.Method,
|
||||||
|
url: []const u8,
|
||||||
|
body: std.ArrayListUnmanaged(u8) = .empty,
|
||||||
|
headers: std.ArrayListUnmanaged([]const u8) = .empty,
|
||||||
|
status: u16 = 0,
|
||||||
|
mime: ?Mime = null,
|
||||||
|
transfer: ?*HttpClient.Transfer = null,
|
||||||
|
|
||||||
|
/// This effectively takes ownership of the FetchContext.
|
||||||
|
///
|
||||||
|
/// We just return the underlying slices used for `headers`
|
||||||
|
/// and for `body` here to avoid an allocation.
|
||||||
|
pub fn toResponse(self: *const FetchContext) !Response {
|
||||||
|
var headers: Headers = .{};
|
||||||
|
|
||||||
|
// convert into Headers
|
||||||
|
for (self.headers.items) |hdr| {
|
||||||
|
var iter = std.mem.splitScalar(u8, hdr, ':');
|
||||||
|
const name = iter.next() orelse "";
|
||||||
|
const value = iter.next() orelse "";
|
||||||
|
try headers.append(name, value, self.arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response{
|
||||||
|
.status = self.status,
|
||||||
|
.headers = headers,
|
||||||
|
.mime = self.mime,
|
||||||
|
.body = self.body.items,
|
||||||
|
.url = self.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
|
||||||
|
pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promise {
|
||||||
|
const arena = page.arena;
|
||||||
|
|
||||||
|
const req = try Request.constructor(input, options, page);
|
||||||
|
var headers = try Http.Headers.init();
|
||||||
|
|
||||||
|
// Copy our headers into the HTTP headers.
|
||||||
|
var header_iter = req.headers.headers.iterator();
|
||||||
|
while (header_iter.next()) |entry| {
|
||||||
|
const combined = try std.fmt.allocPrintSentinel(
|
||||||
|
page.arena,
|
||||||
|
"{s}: {s}",
|
||||||
|
.{ entry.key_ptr.*, entry.value_ptr.* },
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
try headers.add(combined.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers);
|
||||||
|
|
||||||
|
const resolver = try page.main_context.createPersistentPromiseResolver();
|
||||||
|
|
||||||
|
const fetch_ctx = try arena.create(FetchContext);
|
||||||
|
fetch_ctx.* = .{
|
||||||
|
.arena = arena,
|
||||||
|
.js_ctx = page.main_context,
|
||||||
|
.promise_resolver = resolver,
|
||||||
|
.method = req.method,
|
||||||
|
.url = req.url,
|
||||||
|
};
|
||||||
|
|
||||||
|
try page.http_client.request(.{
|
||||||
|
.ctx = @ptrCast(fetch_ctx),
|
||||||
|
.url = req.url,
|
||||||
|
.method = req.method,
|
||||||
|
.headers = headers,
|
||||||
|
.body = req.body,
|
||||||
|
.cookie_jar = page.cookie_jar,
|
||||||
|
.resource_type = .fetch,
|
||||||
|
|
||||||
|
.start_callback = struct {
|
||||||
|
fn startCallback(transfer: *HttpClient.Transfer) !void {
|
||||||
|
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
log.debug(.fetch, "request start", .{ .method = self.method, .url = self.url, .source = "fetch" });
|
||||||
|
|
||||||
|
self.transfer = transfer;
|
||||||
|
}
|
||||||
|
}.startCallback,
|
||||||
|
.header_callback = struct {
|
||||||
|
fn headerCallback(transfer: *HttpClient.Transfer) !void {
|
||||||
|
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
|
||||||
|
const header = &transfer.response_header.?;
|
||||||
|
|
||||||
|
log.debug(.fetch, "request header", .{
|
||||||
|
.source = "fetch",
|
||||||
|
.method = self.method,
|
||||||
|
.url = self.url,
|
||||||
|
.status = header.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (header.contentType()) |ct| {
|
||||||
|
self.mime = Mime.parse(ct) catch {
|
||||||
|
return error.MimeParsing;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transfer.getContentLength()) |cl| {
|
||||||
|
try self.body.ensureTotalCapacity(self.arena, cl);
|
||||||
|
}
|
||||||
|
|
||||||
|
var it = transfer.responseHeaderIterator();
|
||||||
|
while (it.next()) |hdr| {
|
||||||
|
const joined = try std.fmt.allocPrint(self.arena, "{s}: {s}", .{ hdr.name, hdr.value });
|
||||||
|
try self.headers.append(self.arena, joined);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.status = header.status;
|
||||||
|
}
|
||||||
|
}.headerCallback,
|
||||||
|
.data_callback = struct {
|
||||||
|
fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
|
const self: *FetchContext = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
try self.body.appendSlice(self.arena, data);
|
||||||
|
}
|
||||||
|
}.dataCallback,
|
||||||
|
.done_callback = struct {
|
||||||
|
fn doneCallback(ctx: *anyopaque) !void {
|
||||||
|
const self: *FetchContext = @ptrCast(@alignCast(ctx));
|
||||||
|
self.transfer = null;
|
||||||
|
|
||||||
|
log.info(.fetch, "request complete", .{
|
||||||
|
.source = "fetch",
|
||||||
|
.method = self.method,
|
||||||
|
.url = self.url,
|
||||||
|
.status = self.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = try self.toResponse();
|
||||||
|
try self.promise_resolver.resolve(response);
|
||||||
|
}
|
||||||
|
}.doneCallback,
|
||||||
|
.error_callback = struct {
|
||||||
|
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||||
|
const self: *FetchContext = @ptrCast(@alignCast(ctx));
|
||||||
|
self.transfer = null;
|
||||||
|
|
||||||
|
log.err(.fetch, "error", .{
|
||||||
|
.url = self.url,
|
||||||
|
.err = err,
|
||||||
|
.source = "fetch error",
|
||||||
|
});
|
||||||
|
|
||||||
|
// We throw an Abort error when the page is getting closed so,
|
||||||
|
// in this case, we don't need to reject the promise.
|
||||||
|
if (err != error.Abort) {
|
||||||
|
self.promise_resolver.reject(@errorName(err)) catch unreachable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.errorCallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "fetch: fetch" {
|
||||||
|
try testing.htmlRunner("fetch/fetch.html");
|
||||||
|
}
|
||||||
@@ -1042,84 +1042,68 @@ pub const HTMLSlotElement = struct {
|
|||||||
flatten: bool = false,
|
flatten: bool = false,
|
||||||
};
|
};
|
||||||
pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
|
pub fn _assignedNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
|
||||||
return findAssignedSlotNodes(self, opts_, false, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should return Union, instead of NodeUnion, but we want to re-use
|
|
||||||
// findAssignedSlotNodes. Returning NodeUnion is fine, as long as every element
|
|
||||||
// within is an Element. This could be more efficient
|
|
||||||
pub fn _assignedElements(self: *parser.Slot, opts_: ?AssignedNodesOpts, page: *Page) ![]NodeUnion {
|
|
||||||
return findAssignedSlotNodes(self, opts_, true, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn findAssignedSlotNodes(self: *parser.Slot, opts_: ?AssignedNodesOpts, element_only: bool, page: *Page) ![]NodeUnion {
|
|
||||||
const opts = opts_ orelse AssignedNodesOpts{ .flatten = false };
|
const opts = opts_ orelse AssignedNodesOpts{ .flatten = false };
|
||||||
|
|
||||||
if (opts.flatten) {
|
if (try findAssignedSlotNodes(self, opts, page)) |nodes| {
|
||||||
log.debug(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts.flatten) {
|
||||||
|
return &.{};
|
||||||
}
|
}
|
||||||
|
|
||||||
const node: *parser.Node = @ptrCast(@alignCast(self));
|
const node: *parser.Node = @ptrCast(@alignCast(self));
|
||||||
|
const nl = try parser.nodeGetChildNodes(node);
|
||||||
|
const len = try parser.nodeListLength(nl);
|
||||||
|
if (len == 0) {
|
||||||
|
return &.{};
|
||||||
|
}
|
||||||
|
|
||||||
// First we look for any explicitly assigned nodes (via the slot attribute)
|
var assigned = try page.call_arena.alloc(NodeUnion, len);
|
||||||
{
|
var i: usize = 0;
|
||||||
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
|
while (true) : (i += 1) {
|
||||||
var root = try parser.nodeGetRootNode(node);
|
const child = try parser.nodeListItem(nl, @intCast(i)) orelse break;
|
||||||
if (page.getNodeState(root)) |state| {
|
assigned[i] = try Node.toInterface(child);
|
||||||
if (state.shadow_root) |sr| {
|
}
|
||||||
root = @ptrCast(@alignCast(sr.host));
|
return assigned[0..i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn findAssignedSlotNodes(self: *parser.Slot, opts: AssignedNodesOpts, page: *Page) !?[]NodeUnion {
|
||||||
|
if (opts.flatten) {
|
||||||
|
log.warn(.web_api, "not implemented", .{ .feature = "HTMLSlotElement flatten assignedNodes" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const slot_name = try parser.elementGetAttribute(@ptrCast(@alignCast(self)), "name");
|
||||||
|
const node: *parser.Node = @ptrCast(@alignCast(self));
|
||||||
|
var root = try parser.nodeGetRootNode(node);
|
||||||
|
if (page.getNodeState(root)) |state| {
|
||||||
|
if (state.shadow_root) |sr| {
|
||||||
|
root = @ptrCast(@alignCast(sr.host));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var arr: std.ArrayList(NodeUnion) = .empty;
|
var arr: std.ArrayList(NodeUnion) = .empty;
|
||||||
const w = @import("../dom/walker.zig").WalkerChildren{};
|
const w = @import("../dom/walker.zig").WalkerChildren{};
|
||||||
var next: ?*parser.Node = null;
|
var next: ?*parser.Node = null;
|
||||||
while (true) {
|
while (true) {
|
||||||
next = try w.get_next(root, next) orelse break;
|
next = try w.get_next(root, next) orelse break;
|
||||||
if (try parser.nodeType(next.?) != .element) {
|
if (try parser.nodeType(next.?) != .element) {
|
||||||
if (slot_name == null and !element_only) {
|
if (slot_name == null) {
|
||||||
// default slot (with no name), takes everything
|
// default slot (with no name), takes everything
|
||||||
try arr.append(page.call_arena, try Node.toInterface(next.?));
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const el: *parser.Element = @ptrCast(@alignCast(next.?));
|
|
||||||
const element_slot = try parser.elementGetAttribute(el, "slot");
|
|
||||||
|
|
||||||
if (nullableStringsAreEqual(slot_name, element_slot)) {
|
|
||||||
// either they're the same string or they are both null
|
|
||||||
try arr.append(page.call_arena, try Node.toInterface(next.?));
|
try arr.append(page.call_arena, try Node.toInterface(next.?));
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if (arr.items.len > 0) {
|
const el: *parser.Element = @ptrCast(@alignCast(next.?));
|
||||||
return arr.items;
|
const element_slot = try parser.elementGetAttribute(el, "slot");
|
||||||
}
|
|
||||||
|
|
||||||
if (!opts.flatten) {
|
if (nullableStringsAreEqual(slot_name, element_slot)) {
|
||||||
return &.{};
|
// either they're the same string or they are both null
|
||||||
|
try arr.append(page.call_arena, try Node.toInterface(next.?));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return if (arr.items.len == 0) null else arr.items;
|
||||||
// Since, we have no explicitly assigned nodes and flatten == false,
|
|
||||||
// we'll collect the children of the slot - the defaults.
|
|
||||||
{
|
|
||||||
const nl = try parser.nodeGetChildNodes(node);
|
|
||||||
const len = try parser.nodeListLength(nl);
|
|
||||||
if (len == 0) {
|
|
||||||
return &.{};
|
|
||||||
}
|
|
||||||
|
|
||||||
var assigned = try page.call_arena.alloc(NodeUnion, len);
|
|
||||||
var i: usize = 0;
|
|
||||||
while (true) : (i += 1) {
|
|
||||||
const child = try parser.nodeListItem(nl, @intCast(i)) orelse break;
|
|
||||||
if (!element_only or try parser.nodeType(child) == .element) {
|
|
||||||
assigned[i] = try Node.toInterface(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return assigned[0..i];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool {
|
fn nullableStringsAreEqual(a: ?[]const u8, b: ?[]const u8) bool {
|
||||||
@@ -1345,6 +1329,39 @@ test "Browser: HTML.HtmlScriptElement" {
|
|||||||
try testing.htmlRunner("html/script/inline_defer.html");
|
try testing.htmlRunner("html/script/inline_defer.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Browser: HTML.HtmlSlotElement" {
|
test "Browser: HTML.HTMLSlotElement" {
|
||||||
try testing.htmlRunner("html/slot.html");
|
try testing.htmlRunner("html/html_slot_element.html");
|
||||||
|
}
|
||||||
|
|
||||||
|
const Check = struct {
|
||||||
|
input: []const u8,
|
||||||
|
expected: ?[]const u8 = null, // Needed when input != expected
|
||||||
|
};
|
||||||
|
const bool_valids = [_]Check{
|
||||||
|
.{ .input = "true" },
|
||||||
|
.{ .input = "''", .expected = "false" },
|
||||||
|
.{ .input = "13.5", .expected = "true" },
|
||||||
|
};
|
||||||
|
const str_valids = [_]Check{
|
||||||
|
.{ .input = "'foo'", .expected = "foo" },
|
||||||
|
.{ .input = "5", .expected = "5" },
|
||||||
|
.{ .input = "''", .expected = "" },
|
||||||
|
.{ .input = "document", .expected = "[object HTMLDocument]" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// .{ "elem.type = '5'", "5" },
|
||||||
|
// .{ "elem.type", "text" },
|
||||||
|
fn testProperty(
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
runner: *testing.JsRunner,
|
||||||
|
elem_dot_prop: []const u8,
|
||||||
|
always: ?[]const u8, // Ignores checks' expected if set
|
||||||
|
checks: []const Check,
|
||||||
|
) !void {
|
||||||
|
for (checks) |check| {
|
||||||
|
try runner.testCases(&.{
|
||||||
|
.{ try std.mem.concat(arena, u8, &.{ elem_dot_prop, " = ", check.input }), null },
|
||||||
|
.{ elem_dot_prop, always orelse check.expected orelse check.input },
|
||||||
|
}, .{});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ const Css = @import("../css/css.zig").Css;
|
|||||||
const Function = Env.Function;
|
const Function = Env.Function;
|
||||||
const JsObject = Env.JsObject;
|
const JsObject = Env.JsObject;
|
||||||
|
|
||||||
|
const v8 = @import("v8");
|
||||||
|
const Request = @import("../fetch/Request.zig");
|
||||||
|
const fetchFn = @import("../fetch/fetch.zig").fetch;
|
||||||
|
|
||||||
const storage = @import("../storage/storage.zig");
|
const storage = @import("../storage/storage.zig");
|
||||||
|
|
||||||
// https://dom.spec.whatwg.org/#interface-window-extensions
|
// https://dom.spec.whatwg.org/#interface-window-extensions
|
||||||
@@ -95,6 +99,10 @@ pub const Window = struct {
|
|||||||
self.storage_shelf = shelf;
|
self.storage_shelf = shelf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn _fetch(_: *Window, input: Request.RequestInput, options: ?Request.RequestInit, page: *Page) !Env.Promise {
|
||||||
|
return fetchFn(input, options, page);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_window(self: *Window) *Window {
|
pub fn get_window(self: *Window) *Window {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,671 +0,0 @@
|
|||||||
// fetch.js code comes from
|
|
||||||
// https://github.com/JakeChampion/fetch/blob/main/fetch.js
|
|
||||||
//
|
|
||||||
// The original code source is available in MIT license.
|
|
||||||
//
|
|
||||||
// The script comes from the built version from npm.
|
|
||||||
// You can get the package with the command:
|
|
||||||
//
|
|
||||||
// wget $(npm view whatwg-fetch dist.tarball)
|
|
||||||
//
|
|
||||||
// The source is the content of `package/dist/fetch.umd.js` file.
|
|
||||||
(function (global, factory) {
|
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
||||||
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
||||||
(factory((global.WHATWGFetch = {})));
|
|
||||||
}(this, (function (exports) { 'use strict';
|
|
||||||
|
|
||||||
/* eslint-disable no-prototype-builtins */
|
|
||||||
var g =
|
|
||||||
(typeof globalThis !== 'undefined' && globalThis) ||
|
|
||||||
(typeof self !== 'undefined' && self) ||
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
(typeof global !== 'undefined' && global) ||
|
|
||||||
{};
|
|
||||||
|
|
||||||
var support = {
|
|
||||||
searchParams: 'URLSearchParams' in g,
|
|
||||||
iterable: 'Symbol' in g && 'iterator' in Symbol,
|
|
||||||
blob:
|
|
||||||
'FileReader' in g &&
|
|
||||||
'Blob' in g &&
|
|
||||||
(function() {
|
|
||||||
try {
|
|
||||||
new Blob();
|
|
||||||
return true
|
|
||||||
} catch (e) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
formData: 'FormData' in g,
|
|
||||||
|
|
||||||
// Arraybuffer is available but xhr doesn't implement it for now.
|
|
||||||
// arrayBuffer: 'ArrayBuffer' in g
|
|
||||||
arrayBuffer: false
|
|
||||||
};
|
|
||||||
|
|
||||||
function isDataView(obj) {
|
|
||||||
return obj && DataView.prototype.isPrototypeOf(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (support.arrayBuffer) {
|
|
||||||
var viewClasses = [
|
|
||||||
'[object Int8Array]',
|
|
||||||
'[object Uint8Array]',
|
|
||||||
'[object Uint8ClampedArray]',
|
|
||||||
'[object Int16Array]',
|
|
||||||
'[object Uint16Array]',
|
|
||||||
'[object Int32Array]',
|
|
||||||
'[object Uint32Array]',
|
|
||||||
'[object Float32Array]',
|
|
||||||
'[object Float64Array]'
|
|
||||||
];
|
|
||||||
|
|
||||||
var isArrayBufferView =
|
|
||||||
ArrayBuffer.isView ||
|
|
||||||
function(obj) {
|
|
||||||
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeName(name) {
|
|
||||||
if (typeof name !== 'string') {
|
|
||||||
name = String(name);
|
|
||||||
}
|
|
||||||
if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {
|
|
||||||
throw new TypeError('Invalid character in header field name: "' + name + '"')
|
|
||||||
}
|
|
||||||
return name.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeValue(value) {
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
value = String(value);
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build a destructive iterator for the value list
|
|
||||||
function iteratorFor(items) {
|
|
||||||
var iterator = {
|
|
||||||
next: function() {
|
|
||||||
var value = items.shift();
|
|
||||||
return {done: value === undefined, value: value}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (support.iterable) {
|
|
||||||
iterator[Symbol.iterator] = function() {
|
|
||||||
return iterator
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return iterator
|
|
||||||
}
|
|
||||||
|
|
||||||
function Headers(headers) {
|
|
||||||
this.map = {};
|
|
||||||
|
|
||||||
if (headers instanceof Headers) {
|
|
||||||
headers.forEach(function(value, name) {
|
|
||||||
this.append(name, value);
|
|
||||||
}, this);
|
|
||||||
} else if (Array.isArray(headers)) {
|
|
||||||
headers.forEach(function(header) {
|
|
||||||
if (header.length != 2) {
|
|
||||||
throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length)
|
|
||||||
}
|
|
||||||
this.append(header[0], header[1]);
|
|
||||||
}, this);
|
|
||||||
} else if (headers) {
|
|
||||||
Object.getOwnPropertyNames(headers).forEach(function(name) {
|
|
||||||
this.append(name, headers[name]);
|
|
||||||
}, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Headers.prototype.append = function(name, value) {
|
|
||||||
name = normalizeName(name);
|
|
||||||
value = normalizeValue(value);
|
|
||||||
var oldValue = this.map[name];
|
|
||||||
this.map[name] = oldValue ? oldValue + ', ' + value : value;
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype['delete'] = function(name) {
|
|
||||||
delete this.map[normalizeName(name)];
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.get = function(name) {
|
|
||||||
name = normalizeName(name);
|
|
||||||
return this.has(name) ? this.map[name] : null
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.has = function(name) {
|
|
||||||
return this.map.hasOwnProperty(normalizeName(name))
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.set = function(name, value) {
|
|
||||||
this.map[normalizeName(name)] = normalizeValue(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.forEach = function(callback, thisArg) {
|
|
||||||
for (var name in this.map) {
|
|
||||||
if (this.map.hasOwnProperty(name)) {
|
|
||||||
callback.call(thisArg, this.map[name], name, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.keys = function() {
|
|
||||||
var items = [];
|
|
||||||
this.forEach(function(value, name) {
|
|
||||||
items.push(name);
|
|
||||||
});
|
|
||||||
return iteratorFor(items)
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.values = function() {
|
|
||||||
var items = [];
|
|
||||||
this.forEach(function(value) {
|
|
||||||
items.push(value);
|
|
||||||
});
|
|
||||||
return iteratorFor(items)
|
|
||||||
};
|
|
||||||
|
|
||||||
Headers.prototype.entries = function() {
|
|
||||||
var items = [];
|
|
||||||
this.forEach(function(value, name) {
|
|
||||||
items.push([name, value]);
|
|
||||||
});
|
|
||||||
return iteratorFor(items)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (support.iterable) {
|
|
||||||
Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
function consumed(body) {
|
|
||||||
if (body._noBody) return
|
|
||||||
if (body.bodyUsed) {
|
|
||||||
return Promise.reject(new TypeError('Already read'))
|
|
||||||
}
|
|
||||||
body.bodyUsed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileReaderReady(reader) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
reader.onload = function() {
|
|
||||||
resolve(reader.result);
|
|
||||||
};
|
|
||||||
reader.onerror = function() {
|
|
||||||
reject(reader.error);
|
|
||||||
};
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function readBlobAsArrayBuffer(blob) {
|
|
||||||
var reader = new FileReader();
|
|
||||||
var promise = fileReaderReady(reader);
|
|
||||||
reader.readAsArrayBuffer(blob);
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
function readBlobAsText(blob) {
|
|
||||||
var reader = new FileReader();
|
|
||||||
var promise = fileReaderReady(reader);
|
|
||||||
var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type);
|
|
||||||
var encoding = match ? match[1] : 'utf-8';
|
|
||||||
reader.readAsText(blob, encoding);
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
function readArrayBufferAsText(buf) {
|
|
||||||
var view = new Uint8Array(buf);
|
|
||||||
var chars = new Array(view.length);
|
|
||||||
|
|
||||||
for (var i = 0; i < view.length; i++) {
|
|
||||||
chars[i] = String.fromCharCode(view[i]);
|
|
||||||
}
|
|
||||||
return chars.join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
function bufferClone(buf) {
|
|
||||||
if (buf.slice) {
|
|
||||||
return buf.slice(0)
|
|
||||||
} else {
|
|
||||||
var view = new Uint8Array(buf.byteLength);
|
|
||||||
view.set(new Uint8Array(buf));
|
|
||||||
return view.buffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Body() {
|
|
||||||
this.bodyUsed = false;
|
|
||||||
|
|
||||||
this._initBody = function(body) {
|
|
||||||
/*
|
|
||||||
fetch-mock wraps the Response object in an ES6 Proxy to
|
|
||||||
provide useful test harness features such as flush. However, on
|
|
||||||
ES5 browsers without fetch or Proxy support pollyfills must be used;
|
|
||||||
the proxy-pollyfill is unable to proxy an attribute unless it exists
|
|
||||||
on the object before the Proxy is created. This change ensures
|
|
||||||
Response.bodyUsed exists on the instance, while maintaining the
|
|
||||||
semantic of setting Request.bodyUsed in the constructor before
|
|
||||||
_initBody is called.
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-self-assign
|
|
||||||
this.bodyUsed = this.bodyUsed;
|
|
||||||
this._bodyInit = body;
|
|
||||||
if (!body) {
|
|
||||||
this._noBody = true;
|
|
||||||
this._bodyText = '';
|
|
||||||
} else if (typeof body === 'string') {
|
|
||||||
this._bodyText = body;
|
|
||||||
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
|
|
||||||
this._bodyBlob = body;
|
|
||||||
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
|
|
||||||
this._bodyFormData = body;
|
|
||||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
|
||||||
this._bodyText = body.toString();
|
|
||||||
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
|
|
||||||
this._bodyArrayBuffer = bufferClone(body.buffer);
|
|
||||||
// IE 10-11 can't handle a DataView body.
|
|
||||||
this._bodyInit = new Blob([this._bodyArrayBuffer]);
|
|
||||||
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
|
|
||||||
this._bodyArrayBuffer = bufferClone(body);
|
|
||||||
} else {
|
|
||||||
this._bodyText = body = Object.prototype.toString.call(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.headers.get('content-type')) {
|
|
||||||
if (typeof body === 'string') {
|
|
||||||
this.headers.set('content-type', 'text/plain;charset=UTF-8');
|
|
||||||
} else if (this._bodyBlob && this._bodyBlob.type) {
|
|
||||||
this.headers.set('content-type', this._bodyBlob.type);
|
|
||||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
|
||||||
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (support.blob) {
|
|
||||||
this.blob = function() {
|
|
||||||
var rejected = consumed(this);
|
|
||||||
if (rejected) {
|
|
||||||
return rejected
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._bodyBlob) {
|
|
||||||
return Promise.resolve(this._bodyBlob)
|
|
||||||
} else if (this._bodyArrayBuffer) {
|
|
||||||
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
|
|
||||||
} else if (this._bodyFormData) {
|
|
||||||
throw new Error('could not read FormData body as blob')
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(new Blob([this._bodyText]))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.arrayBuffer = function() {
|
|
||||||
if (this._bodyArrayBuffer) {
|
|
||||||
var isConsumed = consumed(this);
|
|
||||||
if (isConsumed) {
|
|
||||||
return isConsumed
|
|
||||||
} else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
|
|
||||||
return Promise.resolve(
|
|
||||||
this._bodyArrayBuffer.buffer.slice(
|
|
||||||
this._bodyArrayBuffer.byteOffset,
|
|
||||||
this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(this._bodyArrayBuffer)
|
|
||||||
}
|
|
||||||
} else if (support.blob) {
|
|
||||||
return this.blob().then(readBlobAsArrayBuffer)
|
|
||||||
} else {
|
|
||||||
throw new Error('could not read as ArrayBuffer')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.text = function() {
|
|
||||||
var rejected = consumed(this);
|
|
||||||
if (rejected) {
|
|
||||||
return rejected
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._bodyBlob) {
|
|
||||||
return readBlobAsText(this._bodyBlob)
|
|
||||||
} else if (this._bodyArrayBuffer) {
|
|
||||||
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
|
|
||||||
} else if (this._bodyFormData) {
|
|
||||||
throw new Error('could not read FormData body as text')
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(this._bodyText)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (support.formData) {
|
|
||||||
this.formData = function() {
|
|
||||||
return this.text().then(decode)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.json = function() {
|
|
||||||
return this.text().then(JSON.parse)
|
|
||||||
};
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP methods whose capitalization should be normalized
|
|
||||||
var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'];
|
|
||||||
|
|
||||||
function normalizeMethod(method) {
|
|
||||||
var upcased = method.toUpperCase();
|
|
||||||
return methods.indexOf(upcased) > -1 ? upcased : method
|
|
||||||
}
|
|
||||||
|
|
||||||
function Request(input, options) {
|
|
||||||
if (!(this instanceof Request)) {
|
|
||||||
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
|
|
||||||
}
|
|
||||||
|
|
||||||
options = options || {};
|
|
||||||
var body = options.body;
|
|
||||||
|
|
||||||
if (input instanceof Request) {
|
|
||||||
if (input.bodyUsed) {
|
|
||||||
throw new TypeError('Already read')
|
|
||||||
}
|
|
||||||
this.url = input.url;
|
|
||||||
this.credentials = input.credentials;
|
|
||||||
if (!options.headers) {
|
|
||||||
this.headers = new Headers(input.headers);
|
|
||||||
}
|
|
||||||
this.method = input.method;
|
|
||||||
this.mode = input.mode;
|
|
||||||
this.signal = input.signal;
|
|
||||||
if (!body && input._bodyInit != null) {
|
|
||||||
body = input._bodyInit;
|
|
||||||
input.bodyUsed = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.url = String(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.credentials = options.credentials || this.credentials || 'same-origin';
|
|
||||||
if (options.headers || !this.headers) {
|
|
||||||
this.headers = new Headers(options.headers);
|
|
||||||
}
|
|
||||||
this.method = normalizeMethod(options.method || this.method || 'GET');
|
|
||||||
this.mode = options.mode || this.mode || null;
|
|
||||||
this.signal = options.signal || this.signal || (function () {
|
|
||||||
if ('AbortController' in g) {
|
|
||||||
var ctrl = new AbortController();
|
|
||||||
return ctrl.signal;
|
|
||||||
}
|
|
||||||
}());
|
|
||||||
this.referrer = null;
|
|
||||||
|
|
||||||
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
|
||||||
throw new TypeError('Body not allowed for GET or HEAD requests')
|
|
||||||
}
|
|
||||||
this._initBody(body);
|
|
||||||
|
|
||||||
if (this.method === 'GET' || this.method === 'HEAD') {
|
|
||||||
if (options.cache === 'no-store' || options.cache === 'no-cache') {
|
|
||||||
// Search for a '_' parameter in the query string
|
|
||||||
var reParamSearch = /([?&])_=[^&]*/;
|
|
||||||
if (reParamSearch.test(this.url)) {
|
|
||||||
// If it already exists then set the value with the current time
|
|
||||||
this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime());
|
|
||||||
} else {
|
|
||||||
// Otherwise add a new '_' parameter to the end with the current time
|
|
||||||
var reQueryString = /\?/;
|
|
||||||
this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Request.prototype.clone = function() {
|
|
||||||
return new Request(this, {body: this._bodyInit})
|
|
||||||
};
|
|
||||||
|
|
||||||
function decode(body) {
|
|
||||||
var form = new FormData();
|
|
||||||
body
|
|
||||||
.trim()
|
|
||||||
.split('&')
|
|
||||||
.forEach(function(bytes) {
|
|
||||||
if (bytes) {
|
|
||||||
var split = bytes.split('=');
|
|
||||||
var name = split.shift().replace(/\+/g, ' ');
|
|
||||||
var value = split.join('=').replace(/\+/g, ' ');
|
|
||||||
form.append(decodeURIComponent(name), decodeURIComponent(value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return form
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseHeaders(rawHeaders) {
|
|
||||||
var headers = new Headers();
|
|
||||||
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
|
|
||||||
// https://tools.ietf.org/html/rfc7230#section-3.2
|
|
||||||
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
|
|
||||||
// Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill
|
|
||||||
// https://github.com/github/fetch/issues/748
|
|
||||||
// https://github.com/zloirock/core-js/issues/751
|
|
||||||
preProcessedHeaders
|
|
||||||
.split('\r')
|
|
||||||
.map(function(header) {
|
|
||||||
return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header
|
|
||||||
})
|
|
||||||
.forEach(function(line) {
|
|
||||||
var parts = line.split(':');
|
|
||||||
var key = parts.shift().trim();
|
|
||||||
if (key) {
|
|
||||||
var value = parts.join(':').trim();
|
|
||||||
try {
|
|
||||||
headers.append(key, value);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Response ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
Body.call(Request.prototype);
|
|
||||||
|
|
||||||
function Response(bodyInit, options) {
|
|
||||||
if (!(this instanceof Response)) {
|
|
||||||
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
|
|
||||||
}
|
|
||||||
if (!options) {
|
|
||||||
options = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.type = 'default';
|
|
||||||
this.status = options.status === undefined ? 200 : options.status;
|
|
||||||
if (this.status < 200 || this.status > 599) {
|
|
||||||
throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].")
|
|
||||||
}
|
|
||||||
this.ok = this.status >= 200 && this.status < 300;
|
|
||||||
this.statusText = options.statusText === undefined ? '' : '' + options.statusText;
|
|
||||||
this.headers = new Headers(options.headers);
|
|
||||||
this.url = options.url || '';
|
|
||||||
this._initBody(bodyInit);
|
|
||||||
}
|
|
||||||
|
|
||||||
Body.call(Response.prototype);
|
|
||||||
|
|
||||||
Response.prototype.clone = function() {
|
|
||||||
return new Response(this._bodyInit, {
|
|
||||||
status: this.status,
|
|
||||||
statusText: this.statusText,
|
|
||||||
headers: new Headers(this.headers),
|
|
||||||
url: this.url
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
Response.error = function() {
|
|
||||||
var response = new Response(null, {status: 200, statusText: ''});
|
|
||||||
response.ok = false;
|
|
||||||
response.status = 0;
|
|
||||||
response.type = 'error';
|
|
||||||
return response
|
|
||||||
};
|
|
||||||
|
|
||||||
var redirectStatuses = [301, 302, 303, 307, 308];
|
|
||||||
|
|
||||||
Response.redirect = function(url, status) {
|
|
||||||
if (redirectStatuses.indexOf(status) === -1) {
|
|
||||||
throw new RangeError('Invalid status code')
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(null, {status: status, headers: {location: url}})
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.DOMException = g.DOMException;
|
|
||||||
try {
|
|
||||||
new exports.DOMException();
|
|
||||||
} catch (err) {
|
|
||||||
exports.DOMException = function(message, name) {
|
|
||||||
this.message = message;
|
|
||||||
this.name = name;
|
|
||||||
var error = Error(message);
|
|
||||||
this.stack = error.stack;
|
|
||||||
};
|
|
||||||
exports.DOMException.prototype = Object.create(Error.prototype);
|
|
||||||
exports.DOMException.prototype.constructor = exports.DOMException;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetch(input, init) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
var request = new Request(input, init);
|
|
||||||
|
|
||||||
if (request.signal && request.signal.aborted) {
|
|
||||||
return reject(new exports.DOMException('Aborted', 'AbortError'))
|
|
||||||
}
|
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
function abortXhr() {
|
|
||||||
xhr.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.onload = function() {
|
|
||||||
var options = {
|
|
||||||
statusText: xhr.statusText,
|
|
||||||
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
|
|
||||||
};
|
|
||||||
// This check if specifically for when a user fetches a file locally from the file system
|
|
||||||
// Only if the status is out of a normal range
|
|
||||||
if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) {
|
|
||||||
options.status = 200;
|
|
||||||
} else {
|
|
||||||
options.status = xhr.status;
|
|
||||||
}
|
|
||||||
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
|
|
||||||
var body = 'response' in xhr ? xhr.response : xhr.responseText;
|
|
||||||
setTimeout(function() {
|
|
||||||
resolve(new Response(body, options));
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onerror = function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
reject(new TypeError('Network request failed'));
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.ontimeout = function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
reject(new TypeError('Network request timed out'));
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onabort = function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
reject(new exports.DOMException('Aborted', 'AbortError'));
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
function fixUrl(url) {
|
|
||||||
try {
|
|
||||||
return url === '' && g.location.href ? g.location.href : url
|
|
||||||
} catch (e) {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.open(request.method, fixUrl(request.url), true);
|
|
||||||
|
|
||||||
if (request.credentials === 'include') {
|
|
||||||
xhr.withCredentials = true;
|
|
||||||
} else if (request.credentials === 'omit') {
|
|
||||||
xhr.withCredentials = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('responseType' in xhr) {
|
|
||||||
if (support.blob) {
|
|
||||||
xhr.responseType = 'blob';
|
|
||||||
} else if (
|
|
||||||
support.arrayBuffer
|
|
||||||
) {
|
|
||||||
xhr.responseType = 'arraybuffer';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
|
|
||||||
var names = [];
|
|
||||||
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
|
|
||||||
names.push(normalizeName(name));
|
|
||||||
xhr.setRequestHeader(name, normalizeValue(init.headers[name]));
|
|
||||||
});
|
|
||||||
request.headers.forEach(function(value, name) {
|
|
||||||
if (names.indexOf(name) === -1) {
|
|
||||||
xhr.setRequestHeader(name, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
request.headers.forEach(function(value, name) {
|
|
||||||
xhr.setRequestHeader(name, value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.signal) {
|
|
||||||
request.signal.addEventListener('abort', abortXhr);
|
|
||||||
|
|
||||||
xhr.onreadystatechange = function() {
|
|
||||||
// DONE (success or failure)
|
|
||||||
if (xhr.readyState === 4) {
|
|
||||||
request.signal.removeEventListener('abort', abortXhr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch.polyfill = true;
|
|
||||||
|
|
||||||
if (!g.fetch) {
|
|
||||||
g.fetch = fetch;
|
|
||||||
g.Headers = Headers;
|
|
||||||
g.Request = Request;
|
|
||||||
g.Response = Response;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.Headers = Headers;
|
|
||||||
exports.Request = Request;
|
|
||||||
exports.Response = Response;
|
|
||||||
exports.fetch = fetch;
|
|
||||||
|
|
||||||
Object.defineProperty(exports, '__esModule', { value: true });
|
|
||||||
|
|
||||||
})));
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// fetch.js code comes from
|
|
||||||
// https://github.com/JakeChampion/fetch/blob/main/fetch.js
|
|
||||||
//
|
|
||||||
// The original code source is available in MIT license.
|
|
||||||
//
|
|
||||||
// The script comes from the built version from npm.
|
|
||||||
// You can get the package with the command:
|
|
||||||
//
|
|
||||||
// wget $(npm view whatwg-fetch dist.tarball)
|
|
||||||
//
|
|
||||||
// The source is the content of `package/dist/fetch.umd.js` file.
|
|
||||||
pub const source = @embedFile("fetch.js");
|
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
|
||||||
test "Browser.fetch" {
|
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
|
||||||
defer runner.deinit();
|
|
||||||
|
|
||||||
try runner.testCases(&.{
|
|
||||||
.{
|
|
||||||
\\ var ok = false;
|
|
||||||
\\ const request = new Request("http://127.0.0.1:9582/loader");
|
|
||||||
\\ fetch(request).then((response) => { ok = response.ok; });
|
|
||||||
\\ false;
|
|
||||||
,
|
|
||||||
"false",
|
|
||||||
},
|
|
||||||
// all events have been resolved.
|
|
||||||
.{ "ok", "true" },
|
|
||||||
}, .{});
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,6 @@ pub const Loader = struct {
|
|||||||
state: enum { empty, loading } = .empty,
|
state: enum { empty, loading } = .empty,
|
||||||
|
|
||||||
done: struct {
|
done: struct {
|
||||||
fetch: bool = false,
|
|
||||||
webcomponents: bool = false,
|
webcomponents: bool = false,
|
||||||
} = .{},
|
} = .{},
|
||||||
|
|
||||||
@@ -56,18 +55,6 @@ pub const Loader = struct {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!self.done.fetch and isFetch(name)) {
|
|
||||||
const source = @import("fetch.zig").source;
|
|
||||||
self.load("fetch", source, js_context);
|
|
||||||
|
|
||||||
// We return false here: We want v8 to continue the calling chain
|
|
||||||
// to finally find the polyfill we just inserted. If we want to
|
|
||||||
// return false and stops the call chain, we have to use
|
|
||||||
// `info.GetReturnValue.Set()` function, or `undefined` will be
|
|
||||||
// returned immediately.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self.done.webcomponents and isWebcomponents(name)) {
|
if (!self.done.webcomponents and isWebcomponents(name)) {
|
||||||
const source = @import("webcomponents.zig").source;
|
const source = @import("webcomponents.zig").source;
|
||||||
self.load("webcomponents", source, js_context);
|
self.load("webcomponents", source, js_context);
|
||||||
@@ -89,14 +76,6 @@ pub const Loader = struct {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isFetch(name: []const u8) bool {
|
|
||||||
if (std.mem.eql(u8, name, "fetch")) return true;
|
|
||||||
if (std.mem.eql(u8, name, "Request")) return true;
|
|
||||||
if (std.mem.eql(u8, name, "Response")) return true;
|
|
||||||
if (std.mem.eql(u8, name, "Headers")) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isWebcomponents(name: []const u8) bool {
|
fn isWebcomponents(name: []const u8) bool {
|
||||||
if (std.mem.eql(u8, name, "customElements")) return true;
|
if (std.mem.eql(u8, name, "customElements")) return true;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
174
src/browser/streams/ReadableStream.zig
Normal file
174
src/browser/streams/ReadableStream.zig
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
const Env = @import("../env.zig").Env;
|
||||||
|
|
||||||
|
const ReadableStream = @This();
|
||||||
|
const ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig");
|
||||||
|
const ReadableStreamDefaultController = @import("ReadableStreamDefaultController.zig");
|
||||||
|
|
||||||
|
const State = union(enum) {
|
||||||
|
readable,
|
||||||
|
closed: ?[]const u8,
|
||||||
|
cancelled: ?[]const u8,
|
||||||
|
errored: Env.JsObject,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This promise resolves when a stream is canceled.
|
||||||
|
cancel_resolver: Env.PersistentPromiseResolver,
|
||||||
|
closed_resolver: Env.PersistentPromiseResolver,
|
||||||
|
reader_resolver: ?Env.PersistentPromiseResolver = null,
|
||||||
|
|
||||||
|
locked: bool = false,
|
||||||
|
state: State = .readable,
|
||||||
|
|
||||||
|
cancel_fn: ?Env.Function = null,
|
||||||
|
pull_fn: ?Env.Function = null,
|
||||||
|
|
||||||
|
strategy: QueueingStrategy,
|
||||||
|
queue: std.ArrayListUnmanaged([]const u8) = .empty,
|
||||||
|
|
||||||
|
pub const ReadableStreamReadResult = struct {
|
||||||
|
const ValueUnion =
|
||||||
|
union(enum) { data: []const u8, empty: void };
|
||||||
|
|
||||||
|
value: ValueUnion,
|
||||||
|
done: bool,
|
||||||
|
|
||||||
|
pub fn get_value(self: *const ReadableStreamReadResult) ValueUnion {
|
||||||
|
return self.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_done(self: *const ReadableStreamReadResult) bool {
|
||||||
|
return self.done;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const UnderlyingSource = struct {
|
||||||
|
start: ?Env.Function = null,
|
||||||
|
pull: ?Env.Function = null,
|
||||||
|
cancel: ?Env.Function = null,
|
||||||
|
type: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const QueueingStrategy = struct {
|
||||||
|
size: ?Env.Function = null,
|
||||||
|
high_water_mark: u32 = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn constructor(underlying: ?UnderlyingSource, _strategy: ?QueueingStrategy, page: *Page) !*ReadableStream {
|
||||||
|
const strategy: QueueingStrategy = _strategy orelse .{};
|
||||||
|
|
||||||
|
const cancel_resolver = try page.main_context.createPersistentPromiseResolver();
|
||||||
|
const closed_resolver = try page.main_context.createPersistentPromiseResolver();
|
||||||
|
|
||||||
|
const stream = try page.arena.create(ReadableStream);
|
||||||
|
stream.* = ReadableStream{ .cancel_resolver = cancel_resolver, .closed_resolver = closed_resolver, .strategy = strategy };
|
||||||
|
|
||||||
|
const controller = ReadableStreamDefaultController{ .stream = stream };
|
||||||
|
|
||||||
|
// call start
|
||||||
|
if (underlying) |src| {
|
||||||
|
if (src.start) |start| {
|
||||||
|
try start.call(void, .{controller});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.cancel) |cancel| {
|
||||||
|
stream.cancel_fn = cancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src.pull) |pull| {
|
||||||
|
stream.pull_fn = pull;
|
||||||
|
try stream.pullIf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn destructor(self: *ReadableStream) void {
|
||||||
|
if (self.reader_resolver) |*rr| {
|
||||||
|
rr.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_locked(self: *const ReadableStream) bool {
|
||||||
|
return self.locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _cancel(self: *ReadableStream, reason: ?[]const u8, page: *Page) !Env.Promise {
|
||||||
|
if (self.locked) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = .{ .cancelled = if (reason) |r| try page.arena.dupe(u8, r) else null };
|
||||||
|
|
||||||
|
// Call cancel callback.
|
||||||
|
if (self.cancel_fn) |cancel| {
|
||||||
|
if (reason) |r| {
|
||||||
|
try cancel.call(void, .{r});
|
||||||
|
} else {
|
||||||
|
try cancel.call(void, .{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.cancel_resolver.resolve({});
|
||||||
|
return self.cancel_resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pullIf(self: *ReadableStream) !void {
|
||||||
|
if (self.pull_fn) |pull_fn| {
|
||||||
|
// Must be under the high water mark AND readable.
|
||||||
|
if ((self.queue.items.len < self.strategy.high_water_mark) and self.state == .readable) {
|
||||||
|
const controller = ReadableStreamDefaultController{ .stream = self };
|
||||||
|
try pull_fn.call(void, .{controller});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetReaderOptions = struct {
|
||||||
|
// Mode must equal 'byob' or be undefined. RangeError otherwise.
|
||||||
|
mode: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn _getReader(self: *ReadableStream, _options: ?GetReaderOptions) !ReadableStreamDefaultReader {
|
||||||
|
if (self.locked) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Determine if we need the ReadableStreamBYOBReader
|
||||||
|
const options = _options orelse GetReaderOptions{};
|
||||||
|
_ = options;
|
||||||
|
|
||||||
|
return ReadableStreamDefaultReader.constructor(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: pipeThrough (requires TransformStream)
|
||||||
|
|
||||||
|
// TODO: pipeTo (requires WritableStream)
|
||||||
|
|
||||||
|
// TODO: tee
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "streams: ReadableStream" {
|
||||||
|
try testing.htmlRunner("streams/readable_stream.html");
|
||||||
|
}
|
||||||
82
src/browser/streams/ReadableStreamDefaultController.zig
Normal file
82
src/browser/streams/ReadableStreamDefaultController.zig
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
const Env = @import("../env.zig").Env;
|
||||||
|
|
||||||
|
const ReadableStream = @import("./ReadableStream.zig");
|
||||||
|
const ReadableStreamReadResult = @import("./ReadableStream.zig").ReadableStreamReadResult;
|
||||||
|
|
||||||
|
const ReadableStreamDefaultController = @This();
|
||||||
|
|
||||||
|
stream: *ReadableStream,
|
||||||
|
|
||||||
|
pub fn get_desiredSize(self: *const ReadableStreamDefaultController) i32 {
|
||||||
|
// TODO: This may need tuning at some point if it becomes a performance issue.
|
||||||
|
return @intCast(self.stream.queue.capacity - self.stream.queue.items.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _close(self: *ReadableStreamDefaultController, _reason: ?[]const u8, page: *Page) !void {
|
||||||
|
const reason = if (_reason) |reason| try page.arena.dupe(u8, reason) else null;
|
||||||
|
self.stream.state = .{ .closed = reason };
|
||||||
|
|
||||||
|
// Resolve the Reader Promise
|
||||||
|
if (self.stream.reader_resolver) |*rr| {
|
||||||
|
defer rr.deinit();
|
||||||
|
try rr.resolve(ReadableStreamReadResult{ .value = .empty, .done = true });
|
||||||
|
self.stream.reader_resolver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the Closed promise.
|
||||||
|
try self.stream.closed_resolver.resolve({});
|
||||||
|
|
||||||
|
// close just sets as closed meaning it wont READ any more but anything in the queue is fine to read.
|
||||||
|
// to discard, must use cancel.
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: []const u8, page: *Page) !void {
|
||||||
|
const stream = self.stream;
|
||||||
|
|
||||||
|
if (stream.state != .readable) {
|
||||||
|
return error.TypeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const duped_chunk = try page.arena.dupe(u8, chunk);
|
||||||
|
|
||||||
|
if (self.stream.reader_resolver) |*rr| {
|
||||||
|
defer rr.deinit();
|
||||||
|
try rr.resolve(ReadableStreamReadResult{ .value = .{ .data = duped_chunk }, .done = false });
|
||||||
|
self.stream.reader_resolver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.stream.queue.append(page.arena, duped_chunk);
|
||||||
|
try self.stream.pullIf();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _error(self: *ReadableStreamDefaultController, err: Env.JsObject) !void {
|
||||||
|
self.stream.state = .{ .errored = err };
|
||||||
|
|
||||||
|
if (self.stream.reader_resolver) |*rr| {
|
||||||
|
defer rr.deinit();
|
||||||
|
try rr.reject(err);
|
||||||
|
self.stream.reader_resolver = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/browser/streams/ReadableStreamDefaultReader.zig
Normal file
96
src/browser/streams/ReadableStreamDefaultReader.zig
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
const Env = @import("../env.zig").Env;
|
||||||
|
const Page = @import("../page.zig").Page;
|
||||||
|
const ReadableStream = @import("./ReadableStream.zig");
|
||||||
|
const ReadableStreamReadResult = @import("./ReadableStream.zig").ReadableStreamReadResult;
|
||||||
|
|
||||||
|
const ReadableStreamDefaultReader = @This();
|
||||||
|
|
||||||
|
stream: *ReadableStream,
|
||||||
|
|
||||||
|
pub fn constructor(stream: *ReadableStream) ReadableStreamDefaultReader {
|
||||||
|
return .{ .stream = stream };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_closed(self: *const ReadableStreamDefaultReader) Env.Promise {
|
||||||
|
return self.stream.closed_resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _cancel(self: *ReadableStreamDefaultReader, reason: ?[]const u8, page: *Page) !Env.Promise {
|
||||||
|
return try self.stream._cancel(reason, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _read(self: *const ReadableStreamDefaultReader, page: *Page) !Env.Promise {
|
||||||
|
const stream = self.stream;
|
||||||
|
|
||||||
|
switch (stream.state) {
|
||||||
|
.readable => {
|
||||||
|
if (stream.queue.items.len > 0) {
|
||||||
|
const data = self.stream.queue.orderedRemove(0);
|
||||||
|
const resolver = page.main_context.createPromiseResolver();
|
||||||
|
|
||||||
|
try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false });
|
||||||
|
try self.stream.pullIf();
|
||||||
|
return resolver.promise();
|
||||||
|
} else {
|
||||||
|
if (self.stream.reader_resolver) |rr| {
|
||||||
|
return rr.promise();
|
||||||
|
} else {
|
||||||
|
const persistent_resolver = try page.main_context.createPersistentPromiseResolver();
|
||||||
|
self.stream.reader_resolver = persistent_resolver;
|
||||||
|
return persistent_resolver.promise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.closed => |_| {
|
||||||
|
const resolver = page.main_context.createPromiseResolver();
|
||||||
|
|
||||||
|
if (stream.queue.items.len > 0) {
|
||||||
|
const data = self.stream.queue.orderedRemove(0);
|
||||||
|
try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false });
|
||||||
|
} else {
|
||||||
|
try resolver.resolve(ReadableStreamReadResult{ .value = .empty, .done = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolver.promise();
|
||||||
|
},
|
||||||
|
.cancelled => |_| {
|
||||||
|
const resolver = page.main_context.createPromiseResolver();
|
||||||
|
try resolver.resolve(ReadableStreamReadResult{ .value = .empty, .done = true });
|
||||||
|
return resolver.promise();
|
||||||
|
},
|
||||||
|
.errored => |err| {
|
||||||
|
const resolver = page.main_context.createPromiseResolver();
|
||||||
|
try resolver.reject(err);
|
||||||
|
return resolver.promise();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _releaseLock(self: *const ReadableStreamDefaultReader) !void {
|
||||||
|
self.stream.locked = false;
|
||||||
|
|
||||||
|
if (self.stream.reader_resolver) |rr| {
|
||||||
|
try rr.reject("TypeError");
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/browser/streams/streams.zig
Normal file
24
src/browser/streams/streams.zig
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
pub const Interfaces = .{
|
||||||
|
@import("ReadableStream.zig"),
|
||||||
|
@import("ReadableStream.zig").ReadableStreamReadResult,
|
||||||
|
@import("ReadableStreamDefaultReader.zig"),
|
||||||
|
@import("ReadableStreamDefaultController.zig"),
|
||||||
|
};
|
||||||
@@ -200,6 +200,7 @@ pub fn requestIntercept(arena: Allocator, bc: anytype, intercept: *const Notific
|
|||||||
.script => "Script",
|
.script => "Script",
|
||||||
.xhr => "XHR",
|
.xhr => "XHR",
|
||||||
.document => "Document",
|
.document => "Document",
|
||||||
|
.fetch => "Fetch",
|
||||||
},
|
},
|
||||||
.networkId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
|
.networkId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
|
||||||
}, .{ .session_id = session_id });
|
}, .{ .session_id = session_id });
|
||||||
@@ -405,6 +406,7 @@ pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Noti
|
|||||||
.script => "Script",
|
.script => "Script",
|
||||||
.xhr => "XHR",
|
.xhr => "XHR",
|
||||||
.document => "Document",
|
.document => "Document",
|
||||||
|
.fetch => "Fetch",
|
||||||
},
|
},
|
||||||
.authChallenge = .{
|
.authChallenge = .{
|
||||||
.source = if (challenge.source == .server) "Server" else "Proxy",
|
.source = if (challenge.source == .server) "Server" else "Proxy",
|
||||||
|
|||||||
@@ -649,6 +649,7 @@ pub const Request = struct {
|
|||||||
document,
|
document,
|
||||||
xhr,
|
xhr,
|
||||||
script,
|
script,
|
||||||
|
fetch,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -339,13 +339,13 @@ pub const Opts = struct {
|
|||||||
proxy_bearer_token: ?[:0]const u8 = null,
|
proxy_bearer_token: ?[:0]const u8 = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Method = enum {
|
pub const Method = enum(u8) {
|
||||||
GET,
|
GET = 0,
|
||||||
PUT,
|
PUT = 1,
|
||||||
POST,
|
POST = 2,
|
||||||
DELETE,
|
DELETE = 3,
|
||||||
HEAD,
|
HEAD = 4,
|
||||||
OPTIONS,
|
OPTIONS = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: on BSD / Linux, we could just read the PEM file directly.
|
// TODO: on BSD / Linux, we could just read the PEM file directly.
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pub const Scope = enum {
|
|||||||
unknown_prop,
|
unknown_prop,
|
||||||
web_api,
|
web_api,
|
||||||
xhr,
|
xhr,
|
||||||
|
fetch,
|
||||||
polyfill,
|
polyfill,
|
||||||
mouse_event,
|
mouse_event,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -677,6 +677,9 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
// we now simply persist every time persist() is called.
|
// we now simply persist every time persist() is called.
|
||||||
js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty,
|
js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty,
|
||||||
|
|
||||||
|
|
||||||
|
persisted_promise_resolvers: std.ArrayListUnmanaged(v8.Persistent(v8.PromiseResolver)) = .empty,
|
||||||
|
|
||||||
// When we need to load a resource (i.e. an external script), we call
|
// When we need to load a resource (i.e. an external script), we call
|
||||||
// this function to get the source. This is always a reference to the
|
// this function to get the source. This is always a reference to the
|
||||||
// Page's fetchModuleSource, but we use a function pointer
|
// Page's fetchModuleSource, but we use a function pointer
|
||||||
@@ -733,6 +736,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
p.deinit();
|
p.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (self.persisted_promise_resolvers.items) |*p| {
|
||||||
|
p.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var it = self.module_cache.valueIterator();
|
var it = self.module_cache.valueIterator();
|
||||||
while (it.next()) |p| {
|
while (it.next()) |p| {
|
||||||
@@ -1130,6 +1137,16 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
|
.array => |arr| {
|
||||||
|
// Retrieve fixed-size array as slice
|
||||||
|
const slice_type = []arr.child;
|
||||||
|
const slice_value = try self.jsValueToZig(named_function, slice_type, js_value);
|
||||||
|
if (slice_value.len != arr.len) {
|
||||||
|
// Exact length match, we could allow smaller arrays, but we would not be able to communicate how many were written
|
||||||
|
return error.InvalidArgument;
|
||||||
|
}
|
||||||
|
return @as(*T, @ptrCast(slice_value.ptr)).*;
|
||||||
|
},
|
||||||
.@"struct" => {
|
.@"struct" => {
|
||||||
return try (self.jsValueToStruct(named_function, T, js_value)) orelse {
|
return try (self.jsValueToStruct(named_function, T, js_value)) orelse {
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
@@ -1251,6 +1268,15 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn createPersistentPromiseResolver(self: *JsContext) !PersistentPromiseResolver {
|
||||||
|
const resolver = v8.Persistent(v8.PromiseResolver).init(self.isolate, v8.PromiseResolver.init(self.v8_context));
|
||||||
|
try self.persisted_promise_resolvers.append(self.context_arena, resolver);
|
||||||
|
return .{
|
||||||
|
.js_context = self,
|
||||||
|
.resolver = resolver,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Probing is part of trying to map a JS value to a Zig union. There's
|
// Probing is part of trying to map a JS value to a Zig union. There's
|
||||||
// a lot of ambiguity in this process, in part because some JS values
|
// a lot of ambiguity in this process, in part because some JS values
|
||||||
// can almost always be coerced. For example, anything can be coerced
|
// can almost always be coerced. For example, anything can be coerced
|
||||||
@@ -1413,6 +1439,36 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
},
|
},
|
||||||
|
.array => |arr| {
|
||||||
|
// Retrieve fixed-size array as slice then probe
|
||||||
|
const slice_type = []arr.child;
|
||||||
|
switch (try self.probeJsValueToZig(named_function, slice_type, js_value)) {
|
||||||
|
.value => |slice_value| {
|
||||||
|
if (slice_value.len == arr.len) {
|
||||||
|
return .{ .value = @as(*T, @ptrCast(slice_value.ptr)).* };
|
||||||
|
}
|
||||||
|
return .{ .invalid = {} };
|
||||||
|
},
|
||||||
|
.ok => {
|
||||||
|
// Exact length match, we could allow smaller arrays as .compatible, but we would not be able to communicate how many were written
|
||||||
|
if (js_value.isArray()) {
|
||||||
|
const js_arr = js_value.castTo(v8.Array);
|
||||||
|
if (js_arr.length() == arr.len) {
|
||||||
|
return .{ .ok = {} };
|
||||||
|
}
|
||||||
|
} else if (js_value.isString() and arr.child == u8) {
|
||||||
|
const str = try js_value.toString(self.v8_context);
|
||||||
|
if (str.lenUtf8(self.isolate) == arr.len) {
|
||||||
|
return .{ .ok = {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .{ .invalid = {} };
|
||||||
|
},
|
||||||
|
.compatible => return .{ .compatible = {} },
|
||||||
|
.coerce => return .{ .coerce = {} },
|
||||||
|
.invalid => return .{ .invalid = {} },
|
||||||
|
}
|
||||||
|
},
|
||||||
.@"struct" => {
|
.@"struct" => {
|
||||||
// We don't want to duplicate the code for this, so we call
|
// We don't want to duplicate the code for this, so we call
|
||||||
// the actual conversion function.
|
// the actual conversion function.
|
||||||
@@ -2178,6 +2234,54 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
|||||||
return error.FailedToResolvePromise;
|
return error.FailedToResolvePromise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reject(self: PromiseResolver, value: anytype) !void {
|
||||||
|
const js_context = self.js_context;
|
||||||
|
const js_value = try js_context.zigValueToJs(value);
|
||||||
|
|
||||||
|
// resolver.reject will return null if the promise isn't pending
|
||||||
|
const ok = self.resolver.reject(js_context.v8_context, js_value) orelse return;
|
||||||
|
if (!ok) {
|
||||||
|
return error.FailedToRejectPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PersistentPromiseResolver = struct {
|
||||||
|
js_context: *JsContext,
|
||||||
|
resolver: v8.Persistent(v8.PromiseResolver),
|
||||||
|
|
||||||
|
pub fn deinit(self: *PersistentPromiseResolver) void {
|
||||||
|
self.resolver.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn promise(self: PersistentPromiseResolver) Promise {
|
||||||
|
return .{
|
||||||
|
.promise = self.resolver.castToPromiseResolver().getPromise(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void {
|
||||||
|
const js_context = self.js_context;
|
||||||
|
const js_value = try js_context.zigValueToJs(value);
|
||||||
|
|
||||||
|
// resolver.resolve will return null if the promise isn't pending
|
||||||
|
const ok = self.resolver.castToPromiseResolver().resolve(js_context.v8_context, js_value) orelse return;
|
||||||
|
if (!ok) {
|
||||||
|
return error.FailedToResolvePromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reject(self: PersistentPromiseResolver, value: anytype) !void {
|
||||||
|
const js_context = self.js_context;
|
||||||
|
const js_value = try js_context.zigValueToJs(value);
|
||||||
|
|
||||||
|
// resolver.reject will return null if the promise isn't pending
|
||||||
|
const ok = self.resolver.castToPromiseResolver().reject(js_context.v8_context, js_value) orelse return;
|
||||||
|
if (!ok) {
|
||||||
|
return error.FailedToRejectPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Promise = struct {
|
pub const Promise = struct {
|
||||||
|
|||||||
16
src/tests/fetch/fetch.html
Normal file
16
src/tests/fetch/fetch.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id=fetch type=module>
|
||||||
|
const promise1 = new Promise((resolve) => {
|
||||||
|
fetch('http://127.0.0.1:9582/xhr/json')
|
||||||
|
.then((res) => {
|
||||||
|
return res.json()
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
resolve(json);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(promise1, (json) => {
|
||||||
|
testing.expectEqual({over: '9000!!!'}, json);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
102
src/tests/fetch/headers.html
Normal file
102
src/tests/fetch/headers.html
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=headers>
|
||||||
|
let headers = new Headers({"Set-Cookie": "name=world"});
|
||||||
|
testing.expectEqual("name=world", headers.get("set-cookie"));
|
||||||
|
|
||||||
|
let myHeaders = new Headers();
|
||||||
|
myHeaders.append("Content-Type", "image/jpeg"),
|
||||||
|
testing.expectEqual(false, myHeaders.has("Picture-Type"));
|
||||||
|
testing.expectEqual("image/jpeg", myHeaders.get("Content-Type"));
|
||||||
|
|
||||||
|
myHeaders.append("Content-Type", "image/png");
|
||||||
|
testing.expectEqual("image/jpeg, image/png", myHeaders.get("Content-Type"));
|
||||||
|
|
||||||
|
myHeaders.delete("Content-Type");
|
||||||
|
testing.expectEqual(null, myHeaders.get("Content-Type"));
|
||||||
|
|
||||||
|
myHeaders.set("Picture-Type", "image/svg")
|
||||||
|
testing.expectEqual("image/svg", myHeaders.get("Picture-Type"));
|
||||||
|
testing.expectEqual(true, myHeaders.has("Picture-Type"))
|
||||||
|
|
||||||
|
const originalHeaders = new Headers([["Content-Type", "application/json"], ["Authorization", "Bearer token123"]]);
|
||||||
|
testing.expectEqual("application/json", originalHeaders.get("Content-Type"));
|
||||||
|
testing.expectEqual("Bearer token123", originalHeaders.get("Authorization"));
|
||||||
|
|
||||||
|
const newHeaders = new Headers(originalHeaders);
|
||||||
|
testing.expectEqual("application/json", newHeaders.get("Content-Type"));
|
||||||
|
testing.expectEqual("Bearer token123" ,newHeaders.get("Authorization"));
|
||||||
|
testing.expectEqual(true ,newHeaders.has("Content-Type"));
|
||||||
|
testing.expectEqual(true ,newHeaders.has("Authorization"));
|
||||||
|
testing.expectEqual(false, newHeaders.has("X-Custom"));
|
||||||
|
|
||||||
|
newHeaders.set("X-Custom", "test-value");
|
||||||
|
testing.expectEqual("test-value", newHeaders.get("X-Custom"));
|
||||||
|
testing.expectEqual(null, originalHeaders.get("X-Custom"));
|
||||||
|
testing.expectEqual(false, originalHeaders.has("X-Custom"));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=keys>
|
||||||
|
const testKeyHeaders = new Headers();
|
||||||
|
testKeyHeaders.set("Content-Type", "application/json");
|
||||||
|
testKeyHeaders.set("Authorization", "Bearer token123");
|
||||||
|
testKeyHeaders.set("X-Custom", "test-value");
|
||||||
|
|
||||||
|
const keys = [];
|
||||||
|
for (const key of testKeyHeaders.keys()) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
testing.expectEqual(3, keys.length);
|
||||||
|
testing.expectEqual(true, keys.includes("Content-Type"));
|
||||||
|
testing.expectEqual(true, keys.includes("Authorization"));
|
||||||
|
testing.expectEqual(true, keys.includes("X-Custom"));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=values>
|
||||||
|
const testValuesHeaders = new Headers();
|
||||||
|
testValuesHeaders.set("Content-Type", "application/json");
|
||||||
|
testValuesHeaders.set("Authorization", "Bearer token123");
|
||||||
|
testValuesHeaders.set("X-Custom", "test-value");
|
||||||
|
|
||||||
|
const values = [];
|
||||||
|
for (const value of testValuesHeaders.values()) {
|
||||||
|
values.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
testing.expectEqual(3, values.length);
|
||||||
|
testing.expectEqual(true, values.includes("application/json"));
|
||||||
|
testing.expectEqual(true, values.includes("Bearer token123"));
|
||||||
|
testing.expectEqual(true, values.includes("test-value"));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=entries>
|
||||||
|
const testEntriesHeaders = new Headers();
|
||||||
|
testEntriesHeaders.set("Content-Type", "application/json");
|
||||||
|
testEntriesHeaders.set("Authorization", "Bearer token123");
|
||||||
|
testEntriesHeaders.set("X-Custom", "test-value");
|
||||||
|
|
||||||
|
const entries = [];
|
||||||
|
for (const entry of testEntriesHeaders.entries()) {
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
testing.expectEqual(3, entries.length);
|
||||||
|
|
||||||
|
const entryMap = new Map(entries);
|
||||||
|
testing.expectEqual("application/json", entryMap.get("Content-Type"));
|
||||||
|
testing.expectEqual("Bearer token123", entryMap.get("Authorization"));
|
||||||
|
testing.expectEqual("test-value", entryMap.get("X-Custom"));
|
||||||
|
|
||||||
|
const entryKeys = Array.from(entryMap.keys());
|
||||||
|
testing.expectEqual(3, entryKeys.length);
|
||||||
|
testing.expectEqual(true, entryKeys.includes("Content-Type"));
|
||||||
|
testing.expectEqual(true, entryKeys.includes("Authorization"));
|
||||||
|
testing.expectEqual(true, entryKeys.includes("X-Custom"));
|
||||||
|
|
||||||
|
const entryValues = Array.from(entryMap.values());
|
||||||
|
testing.expectEqual(3, entryValues.length);
|
||||||
|
testing.expectEqual(true, entryValues.includes("application/json"));
|
||||||
|
testing.expectEqual(true, entryValues.includes("Bearer token123"));
|
||||||
|
testing.expectEqual(true, entryValues.includes("test-value"))
|
||||||
|
</script>
|
||||||
22
src/tests/fetch/request.html
Normal file
22
src/tests/fetch/request.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=request>
|
||||||
|
let request = new Request("flower.png");
|
||||||
|
testing.expectEqual("http://localhost:9582/src/tests/fetch/flower.png", request.url);
|
||||||
|
testing.expectEqual("GET", request.method);
|
||||||
|
|
||||||
|
let request2 = new Request("https://google.com", {
|
||||||
|
method: "POST",
|
||||||
|
body: "Hello, World",
|
||||||
|
cache: "reload",
|
||||||
|
credentials: "omit",
|
||||||
|
headers: { "Sender": "me", "Target": "you" }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
testing.expectEqual("https://google.com", request2.url);
|
||||||
|
testing.expectEqual("POST", request2.method);
|
||||||
|
testing.expectEqual("omit", request2.credentials);
|
||||||
|
testing.expectEqual("reload", request2.cache);
|
||||||
|
testing.expectEqual("me", request2.headers.get("SeNdEr"));
|
||||||
|
testing.expectEqual("you", request2.headers.get("target"));
|
||||||
|
</script>
|
||||||
49
src/tests/fetch/response.html
Normal file
49
src/tests/fetch/response.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=response>
|
||||||
|
let response = new Response("Hello, World!");
|
||||||
|
testing.expectEqual(200, response.status);
|
||||||
|
testing.expectEqual("", response.statusText);
|
||||||
|
testing.expectEqual(true, response.ok);
|
||||||
|
testing.expectEqual("", response.url);
|
||||||
|
testing.expectEqual(false, response.redirected);
|
||||||
|
|
||||||
|
let response2 = new Response("Error occurred", {
|
||||||
|
status: 404,
|
||||||
|
statusText: "Not Found",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
"X-Custom": "test-value",
|
||||||
|
"Cache-Control": "no-cache"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
testing.expectEqual(404, response2.status);
|
||||||
|
testing.expectEqual("Not Found", response2.statusText);
|
||||||
|
testing.expectEqual(false, response2.ok);
|
||||||
|
testing.expectEqual("text/plain", response2.headers.get("Content-Type"));
|
||||||
|
testing.expectEqual("test-value", response2.headers.get("X-Custom"));
|
||||||
|
testing.expectEqual("no-cache", response2.headers.get("cache-control"));
|
||||||
|
|
||||||
|
let response3 = new Response("Created", { status: 201, statusText: "Created" });
|
||||||
|
testing.expectEqual(201, response3.status);
|
||||||
|
testing.expectEqual("Created", response3.statusText);
|
||||||
|
testing.expectEqual(true, response3.ok);
|
||||||
|
|
||||||
|
let nullResponse = new Response(null);
|
||||||
|
testing.expectEqual(200, nullResponse.status);
|
||||||
|
testing.expectEqual("", nullResponse.statusText);
|
||||||
|
|
||||||
|
let emptyResponse = new Response("");
|
||||||
|
testing.expectEqual(200, emptyResponse.status);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=json type=module>
|
||||||
|
const promise1 = new Promise((resolve) => {
|
||||||
|
let response = new Response('[]');
|
||||||
|
response.json().then(resolve)
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(promise1, (json) => {
|
||||||
|
testing.expectEqual([], json);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
66
src/tests/html/html_slot_element.html
Normal file
66
src/tests/html/html_slot_element.html
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script>
|
||||||
|
class LightPanda extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
const shadow = this.attachShadow({ mode: "open" });
|
||||||
|
|
||||||
|
const slot1 = document.createElement('slot');
|
||||||
|
slot1.name = 'slot-1';
|
||||||
|
shadow.appendChild(slot1);
|
||||||
|
|
||||||
|
switch (this.getAttribute('mode')) {
|
||||||
|
case '1':
|
||||||
|
slot1.innerHTML = 'hello';
|
||||||
|
break;
|
||||||
|
case '2':
|
||||||
|
const slot2 = document.createElement('slot');
|
||||||
|
shadow.appendChild(slot2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.customElements.define("lp-test", LightPanda);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<lp-test id=lp1 mode=1></lp-test>
|
||||||
|
<lp-test id=lp2 mode=0></lp-test>
|
||||||
|
<lp-test id=lp3 mode=0>default</lp-test>
|
||||||
|
<lp-test id=lp4 mode=1><p slot=other>default</p></lp-test>
|
||||||
|
<lp-test id=lp5 mode=1><p slot=slot-1>default</p> xx <b slot=slot-1>other</b></lp-test>
|
||||||
|
<lp-test id=lp6 mode=2>More <p slot=slot-1>default2</p> <span>!!</span></lp-test>
|
||||||
|
|
||||||
|
<script id=HTMLSlotElement>
|
||||||
|
function assertNodes(expected, actual) {
|
||||||
|
actual = actual.map((n) => n.id || n.textContent)
|
||||||
|
testing.expectEqual(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let idx of [1, 2, 3, 4]) {
|
||||||
|
const lp = $(`#lp${idx}`);
|
||||||
|
const slot = lp.shadowRoot.querySelector('slot');
|
||||||
|
|
||||||
|
assertNodes([], slot.assignedNodes());
|
||||||
|
assertNodes([], slot.assignedNodes({}));
|
||||||
|
assertNodes([], slot.assignedNodes({flatten: false}));
|
||||||
|
if (lp.getAttribute('mode') === '1') {
|
||||||
|
assertNodes(['hello'], slot.assignedNodes({flatten: true}));
|
||||||
|
} else {
|
||||||
|
assertNodes([], slot.assignedNodes({flatten: true}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lp5 = $('#lp5');
|
||||||
|
const s5 = lp5.shadowRoot.querySelector('slot');
|
||||||
|
assertNodes(['default', 'other'], s5.assignedNodes());
|
||||||
|
|
||||||
|
const lp6 = $('#lp6');
|
||||||
|
const s6 = lp6.shadowRoot.querySelectorAll('slot');
|
||||||
|
assertNodes(['default2'], s6[0].assignedNodes({}));
|
||||||
|
assertNodes(['default2'], s6[0].assignedNodes({flatten: true}));
|
||||||
|
assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({}));
|
||||||
|
assertNodes(['More ', ' ', '!!'], s6[1].assignedNodes({flatten: true}));
|
||||||
|
</script>
|
||||||
117
src/tests/streams/readable_stream.html
Normal file
117
src/tests/streams/readable_stream.html
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=readable_stream>
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue("hello");
|
||||||
|
controller.enqueue("world");
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const reader = stream.getReader();
|
||||||
|
|
||||||
|
testing.async(reader.read(), (data) => {
|
||||||
|
testing.expectEqual("hello", data.value);
|
||||||
|
testing.expectEqual(false, data.done);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readable_stream_close>
|
||||||
|
var closeResult;
|
||||||
|
|
||||||
|
const stream1 = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue("first");
|
||||||
|
controller.enqueue("second");
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const reader1 = stream1.getReader();
|
||||||
|
|
||||||
|
testing.async(reader1.read(), (data) => {
|
||||||
|
testing.expectEqual("first", data.value);
|
||||||
|
testing.expectEqual(false, data.done);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(reader1.read(), (data) => {
|
||||||
|
testing.expectEqual("second", data.value);
|
||||||
|
testing.expectEqual(false, data.done);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(reader1.read(), (data) => {
|
||||||
|
testing.expectEqual(undefined, data.value);
|
||||||
|
testing.expectEqual(true, data.done);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readable_stream_cancel>
|
||||||
|
var readResult;
|
||||||
|
var cancelResult;
|
||||||
|
var closeResult;
|
||||||
|
|
||||||
|
const stream2 = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue("data1");
|
||||||
|
controller.enqueue("data2");
|
||||||
|
controller.enqueue("data3");
|
||||||
|
},
|
||||||
|
cancel(reason) {
|
||||||
|
closeResult = `Stream cancelled: ${reason}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const reader2 = stream2.getReader();
|
||||||
|
|
||||||
|
testing.async(reader2.read(), (data) => {
|
||||||
|
testing.expectEqual("data1", data.value);
|
||||||
|
testing.expectEqual(false, data.done);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(reader2.cancel("user requested"), (result) => {
|
||||||
|
testing.expectEqual(undefined, result);
|
||||||
|
testing.expectEqual("Stream cancelled: user requested", closeResult);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readable_stream_cancel_no_reason>
|
||||||
|
var closeResult2;
|
||||||
|
|
||||||
|
const stream3 = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue("test");
|
||||||
|
},
|
||||||
|
cancel(reason) {
|
||||||
|
closeResult2 = reason === undefined ? "no reason" : reason;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const reader3 = stream3.getReader();
|
||||||
|
|
||||||
|
testing.async(reader3.cancel(), (result) => {
|
||||||
|
testing.expectEqual(undefined, result);
|
||||||
|
testing.expectEqual("no reason", closeResult2);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readable_stream_read_after_cancel>
|
||||||
|
var readAfterCancelResult;
|
||||||
|
|
||||||
|
const stream4 = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue("before_cancel");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const reader4 = stream4.getReader();
|
||||||
|
|
||||||
|
testing.async(reader4.cancel("test cancel"), (cancelResult) => {
|
||||||
|
testing.expectEqual(undefined, cancelResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.async(reader4.read(), (data) => {
|
||||||
|
testing.expectEqual(undefined, data.value);
|
||||||
|
testing.expectEqual(true, data.done);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user