get fetch campire working

This commit is contained in:
Karl Seguin
2025-10-28 11:24:29 +08:00
parent b047cb6dc1
commit cdd31353c5
15 changed files with 142 additions and 46 deletions

View File

@@ -2,10 +2,14 @@ const std = @import("std");
const Node = @import("webapi/Node.zig");
pub const Opts = struct {
// @ZIGDOM (none of these do anything)
with_base: bool = false,
strip_mode: StripMode = .{},
const StripMode = struct {
// @ZIGDOM
pub const StripMode = struct {
js: bool = false,
ui: bool = false,
css: bool = false,
};
};

View File

@@ -387,7 +387,6 @@ pub fn throw(self: *Context, err: []const u8) js.Exception {
pub fn zigValueToJs(self: *Context, value: anytype, comptime opts: Caller.CallOpts) !v8.Value {
const isolate = self.isolate;
// Check if it's a "simple" type. This is extracted so that it can be
// reused by other parts of the code. "simple" types only require an
// isolate to create (specifically, they don't our templates array)
@@ -595,7 +594,6 @@ pub fn mapZigInstanceToJs(self: *Context, js_obj_: ?v8.Object, value: anytype) !
};
const JsApi = bridge.Struct(ptr.child).JsApi;
// The TAO contains the pointer to our Zig instance as
// well as any meta data we'll need to use it later.
// See the TaggedAnyOpaque struct for more details.

View File

@@ -311,7 +311,6 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem
return template;
}
// ZIGDOM (HTMLAllCollection I think)
// fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void {
// const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction");

View File

@@ -401,7 +401,6 @@ pub const SubType = enum {
webassemblymemory,
};
pub const JsApis = flattenTypes(&.{
@import("../webapi/AbortController.zig"),
@import("../webapi/AbortSignal.zig"),

View File

@@ -106,7 +106,7 @@ pub const PersistentPromiseResolver = struct {
pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
const js_value = try context.zigValueToJs(value, .{});
// resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return;
@@ -117,7 +117,7 @@ pub const PersistentPromiseResolver = struct {
pub fn reject(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context;
const js_value = try context.zigValueToJs(value);
const js_value = try context.zigValueToJs(value, .{});
// resolver.reject will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) orelse return;

View File

@@ -58,6 +58,10 @@ _parse_mode: enum { document, fragment },
// even thoug we'll create very few (if any) actual *Attributes.
_attribute_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute),
// Same as _atlribute_lookup, but instead of individual attributes, this is for
// the return of elements.attributes.
_attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap),
_script_manager: ScriptManager,
_polyfill_loader: polyfill.Loader = .{},
@@ -119,6 +123,7 @@ pub fn deinit(self: *Page) void {
log.debug(.page, "page.deinit", .{ .url = self.url });
}
self.js.deinit();
self._script_manager.deinit();
}
fn reset(self: *Page, comptime initializing: bool) !void {
@@ -144,6 +149,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._parse_state = .pre;
self._load_state = .parsing;
self._attribute_lookup = .empty;
self._attribute_named_node_map_lookup = .empty;
self._event_manager = EventManager.init(self);
self._script_manager = ScriptManager.init(self);
@@ -165,7 +171,7 @@ fn registerBackgroundTasks(self: *Page) !void {
const Browser = @import("Browser.zig");
try self.scheduler.add(self._session.browser, struct {
fn runMicrotasks(ctx: *anyopaque) ?u32 {
fn runMicrotasks(ctx: *anyopaque) !?u32 {
const b: *Browser = @ptrCast(@alignCast(ctx));
b.runMicrotasks();
return 5;
@@ -173,7 +179,7 @@ fn registerBackgroundTasks(self: *Page) !void {
}.runMicrotasks, 5, .{ .name = "page.microtasks" });
try self.scheduler.add(self._session.browser, struct {
fn runMessageLoop(ctx: *anyopaque) ?u32 {
fn runMessageLoop(ctx: *anyopaque) !?u32 {
const b: *Browser = @ptrCast(@alignCast(ctx));
b.runMessageLoop();
return 100;
@@ -992,7 +998,7 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi
if (@TypeOf(list) == ?*Element.Attribute.List) {
// from cloneNode
var existing = list orelse return ;
var existing = list orelse return;
var attributes = try self.arena.create(Element.Attribute.List);
attributes.* = .{};

View File

@@ -57,7 +57,7 @@
const firstScript = document.querySelector('script');
testing.expectEqual('SCRIPT', firstScript.tagName);
testing.expectEqual(null, document.querySelector('select'));
testing.expectEqual(null, document.querySelector('article'));
testing.expectEqual(null, document.querySelector('another'));
}
</script>

View File

@@ -170,7 +170,7 @@ pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?u32, fil
return DOMTreeWalker.init(root, show, filter, page);
}
// @ZIGDOM what_to_show tristate (null vs undefined vs value)
// @ZIGDOM what_to_show tristate (null vs undefined vs value)
pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator {
const show = what_to_show orelse NodeFilter.SHOW_ALL;
return DOMNodeIterator.init(root, show, filter, page);

View File

@@ -118,7 +118,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
.script => "script",
.select => "select",
.style => "style",
.text_area => "textara",
.text_area => "textarea",
.title => "title",
.ul => "ul",
.unknown => |e| e._tag_name.str(),
@@ -311,9 +311,14 @@ pub fn getAttributeNames(self: *const Element, page: *Page) ![][]const u8 {
return attributes.getNames(page);
}
pub fn getAttributeNamedNodeMap(self: *Element) Attribute.NamedNodeMap {
const attributes = self._attributes orelse return .{};
return .{ ._list = attributes.*, ._element = self };
pub fn getAttributeNamedNodeMap(self: *Element, page: *Page) !*Attribute.NamedNodeMap {
const gop = try page._attribute_named_node_map_lookup.getOrPut(page.arena, @intFromPtr(self));
if (!gop.found_existing) {
const attributes = try self.getOrCreateAttributeList(page);
const named_node_map = try page._factory.create(Attribute.NamedNodeMap{ ._list = attributes, ._element = self });
gop.value_ptr.* = named_node_map;
}
return gop.value_ptr.*;
}
pub fn getStyle(self: *Element, page: *Page) !*CSSStyleProperties {

View File

@@ -326,7 +326,7 @@ fn needsLowerCasing(name: []const u8) bool {
}
pub const NamedNodeMap = struct {
_list: List = .{},
_list: *List,
// Whenever the NamedNodeMap creates an Attribute, it needs to provide the
// "ownerElement".
@@ -418,6 +418,12 @@ pub const InnerIterator = struct {
fn formatAttribute(name: []const u8, value: []const u8, writer: *std.Io.Writer) !void {
try writer.writeAll(name);
// Boolean attributes with empty values are serialized without a value
if (value.len == 0 and boolean_attributes_lookup.has(name)) {
return;
}
try writer.writeByte('=');
if (value.len == 0) {
return writer.writeAll("\"\"");
@@ -433,6 +439,37 @@ fn formatAttribute(name: []const u8, value: []const u8, writer: *std.Io.Writer)
return writer.writeByte('"');
}
const boolean_attributes = [_][]const u8{
"checked",
"disabled",
"required",
"readonly",
"multiple",
"selected",
"autofocus",
"autoplay",
"controls",
"loop",
"muted",
"hidden",
"async",
"defer",
"novalidate",
"formnovalidate",
"ismap",
"reversed",
"default",
"open",
};
const boolean_attributes_lookup = std.StaticStringMap(void).initComptime(blk: {
var entries: [boolean_attributes.len]struct { []const u8, void } = undefined;
for (boolean_attributes, 0..) |attr, i| {
entries[i] = .{ attr, {} };
}
break :blk entries;
});
fn writeEscapedAttributeValue(value: []const u8, first_offset: usize, writer: *std.Io.Writer) !void {
// Write everything before the first special character
try writer.writeAll(value[0..first_offset]);

View File

@@ -1,5 +1,8 @@
const std = @import("std");
const log = @import("../../../log.zig");
const Http = @import("../../../http/Http.zig");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
@@ -8,15 +11,64 @@ const Response = @import("Response.zig");
const Allocator = std.mem.Allocator;
_arena: Allocator,
_promise: js.Promise,
_has_response: bool,
const Fetch = @This();
_page: *Page,
_response: std.ArrayList(u8),
_resolver: js.PersistentPromiseResolver,
pub const Input = Request.Input;
// @ZIGDOM just enough to get campire demo working
pub fn init(input: Input, page: *Page) !js.Promise {
// @ZIGDOM
_ = input;
_ = page;
return undefined;
const request = try Request.init(input, page);
const fetch = try page.arena.create(Fetch);
fetch.* = .{
._page = page,
._response = .empty,
._resolver = try page.js.createPromiseResolver(.page),
};
const http_client = page._session.browser.http_client;
const headers = try http_client.newHeaders();
try http_client.request(.{
.ctx = fetch,
.url = request._url,
.method = .GET,
.headers = headers,
.cookie_jar = &page._session.cookie_jar,
.resource_type = .fetch,
.header_callback = httpHeaderDoneCallback,
.data_callback = httpDataCallback,
.done_callback = httpDoneCallback,
.error_callback = httpErrorCallback,
});
return fetch._resolver.promise();
}
fn httpHeaderDoneCallback(transfer: *Http.Transfer) !void {
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
_ = self;
}
fn httpDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
try self._response.appendSlice(self._page.arena, data);
}
fn httpDoneCallback(ctx: *anyopaque) !void {
const self: *Fetch = @ptrCast(@alignCast(ctx));
const page = self._page;
const res = try Response.initFromFetch(page.arena, self._response.items, page);
return self._resolver.resolve(res);
}
fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *Fetch = @ptrCast(@alignCast(ctx));
self._resolver.reject(@errorName(err)) catch |inner| {
log.err(.bug, "failed to reject", .{ .source = "fetch", .err = inner, .reject = err });
};
}

View File

@@ -35,7 +35,7 @@ pub fn getJson(self: *Response, page: *Page) !js.Promise {
) catch |err| {
return page.js.rejectPromise(.{@errorName(err)});
};
return page.js.resolvePromise(.{value});
return page.js.resolvePromise(value);
}
pub const JsApi = struct {

View File

@@ -8,8 +8,8 @@ const Allocator = std.mem.Allocator;
pub const FetchOpts = struct {
wait_ms: u32 = 5000,
dump_opts: dump.Opts,
dump_file: ?std.fs.File = null,
dump: dump.Opts,
writer: ?*std.Io.Writer = null,
};
pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
const Browser = @import("browser/Browser.zig");
@@ -40,12 +40,9 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void {
_ = try page.navigate(url, .{});
_ = session.fetchWait(opts.wait_ms);
const file = opts.dump_file orelse return;
var buf: [4096]u8 = undefined;
var writer = file.writer(&buf);
try dump.deep(page.document.asNode(), opts.dump_opts, &writer.interface);
try writer.interface.flush();
const writer = opts.writer orelse return;
try dump.deep(page.document.asNode(), opts.dump, writer);
try writer.flush();
}
test {

View File

@@ -352,7 +352,7 @@ fn elapsed() struct { time: f64, unit: []const u8 } {
}
const datetime = @import("datetime.zig");
fn timestamp(mode: datetime.TimestampMode) u64 {
fn timestamp(comptime mode: datetime.TimestampMode) u64 {
if (comptime @import("builtin").is_test) {
return 1739795092929;
}

View File

@@ -38,19 +38,17 @@ pub fn main() !void {
if (gpa.detectLeaks()) std.posix.exit(1);
};
var global_allocator = lp.GlobalAllocator.init(allocator);
// arena for main-specific allocations
var main_arena = std.heap.ArenaAllocator.init(global_allocator.allocator());
var main_arena = std.heap.ArenaAllocator.init(allocator);
defer main_arena.deinit();
run(&global_allocator, main_arena.allocator()) catch |err| {
run(allocator, main_arena.allocator()) catch |err| {
log.fatal(.app, "exit", .{ .err = err });
std.posix.exit(1);
};
}
fn run(allocator: *lp.GlobalAllocator, main_arena: Allocator) !void {
fn run(allocator: Allocator, main_arena: Allocator) !void {
const args = try parseArgs(main_arena);
switch (args.mode) {
@@ -102,7 +100,6 @@ fn run(allocator: *lp.GlobalAllocator, main_arena: Allocator) !void {
switch (args.mode) {
.serve => {
log.fatal(.app, "serve not not supported in the zigdom branch yet\n", .{});
return;
// @ZIGDOM-CDP
// .serve => |opts| {
@@ -131,13 +128,15 @@ fn run(allocator: *lp.GlobalAllocator, main_arena: Allocator) !void {
var fetch_opts = lp.FetchOpts{
.wait_ms = 5000,
.dump = .{
.with_base = opts.with_base,
.with_base = opts.withbase,
.strip_mode = opts.strip_mode,
},
};
var stdout = std.fs.File.stdout();
var writer = stdout.writer(&.{});
if (opts.dump) {
fetch_opts.dump_file = std.fs.File.stdout();
fetch_opts.writer = &writer.interface;
}
lp.fetch(app, url, fetch_opts) catch |err| {
@@ -245,7 +244,7 @@ const Command = struct {
};
const Fetch = struct {
url: []const u8,
url: [:0]const u8,
dump: bool = false,
common: Common,
withbase: bool = false,
@@ -513,7 +512,7 @@ fn parseFetchArgs(
) !Command.Fetch {
var dump: bool = false;
var withbase: bool = false;
var url: ?[]const u8 = null;
var url: ?[:0]const u8 = null;
var common: Command.Common = .{};
var strip_mode: lp.dump.Opts.StripMode = .{};
@@ -576,7 +575,7 @@ fn parseFetchArgs(
log.fatal(.app, "duplicate fetch url", .{ .help = "only 1 URL can be specified" });
return error.TooManyURLs;
}
url = try allocator.dupe(u8, opt);
url = try allocator.dupeZ(u8, opt);
}
if (url == null) {