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"); const Node = @import("webapi/Node.zig");
pub const Opts = struct { pub const Opts = struct {
// @ZIGDOM (none of these do anything)
with_base: bool = false,
strip_mode: StripMode = .{}, strip_mode: StripMode = .{},
const StripMode = struct { pub const StripMode = struct {
// @ZIGDOM 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 { pub fn zigValueToJs(self: *Context, value: anytype, comptime opts: Caller.CallOpts) !v8.Value {
const isolate = self.isolate; const isolate = self.isolate;
// Check if it's a "simple" type. This is extracted so that it can be // 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 // reused by other parts of the code. "simple" types only require an
// isolate to create (specifically, they don't our templates array) // 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; const JsApi = bridge.Struct(ptr.child).JsApi;
// The TAO contains the pointer to our Zig instance as // The TAO contains the pointer to our Zig instance as
// well as any meta data we'll need to use it later. // well as any meta data we'll need to use it later.
// See the TaggedAnyOpaque struct for more details. // 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; return template;
} }
// ZIGDOM (HTMLAllCollection I think) // ZIGDOM (HTMLAllCollection I think)
// fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void { // fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void {
// const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction"); // const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction");

View File

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

View File

@@ -106,7 +106,7 @@ pub const PersistentPromiseResolver = struct {
pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void { pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context; 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 // resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return; 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 { pub fn reject(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context; 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 // resolver.reject will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) orelse return; 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. // even thoug we'll create very few (if any) actual *Attributes.
_attribute_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute), _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, _script_manager: ScriptManager,
_polyfill_loader: polyfill.Loader = .{}, _polyfill_loader: polyfill.Loader = .{},
@@ -119,6 +123,7 @@ pub fn deinit(self: *Page) void {
log.debug(.page, "page.deinit", .{ .url = self.url }); log.debug(.page, "page.deinit", .{ .url = self.url });
} }
self.js.deinit(); self.js.deinit();
self._script_manager.deinit();
} }
fn reset(self: *Page, comptime initializing: bool) !void { 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._parse_state = .pre;
self._load_state = .parsing; self._load_state = .parsing;
self._attribute_lookup = .empty; self._attribute_lookup = .empty;
self._attribute_named_node_map_lookup = .empty;
self._event_manager = EventManager.init(self); self._event_manager = EventManager.init(self);
self._script_manager = ScriptManager.init(self); self._script_manager = ScriptManager.init(self);
@@ -165,7 +171,7 @@ fn registerBackgroundTasks(self: *Page) !void {
const Browser = @import("Browser.zig"); const Browser = @import("Browser.zig");
try self.scheduler.add(self._session.browser, struct { try self.scheduler.add(self._session.browser, struct {
fn runMicrotasks(ctx: *anyopaque) ?u32 { fn runMicrotasks(ctx: *anyopaque) !?u32 {
const b: *Browser = @ptrCast(@alignCast(ctx)); const b: *Browser = @ptrCast(@alignCast(ctx));
b.runMicrotasks(); b.runMicrotasks();
return 5; return 5;
@@ -173,7 +179,7 @@ fn registerBackgroundTasks(self: *Page) !void {
}.runMicrotasks, 5, .{ .name = "page.microtasks" }); }.runMicrotasks, 5, .{ .name = "page.microtasks" });
try self.scheduler.add(self._session.browser, struct { try self.scheduler.add(self._session.browser, struct {
fn runMessageLoop(ctx: *anyopaque) ?u32 { fn runMessageLoop(ctx: *anyopaque) !?u32 {
const b: *Browser = @ptrCast(@alignCast(ctx)); const b: *Browser = @ptrCast(@alignCast(ctx));
b.runMessageLoop(); b.runMessageLoop();
return 100; return 100;
@@ -992,7 +998,7 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi
if (@TypeOf(list) == ?*Element.Attribute.List) { if (@TypeOf(list) == ?*Element.Attribute.List) {
// from cloneNode // from cloneNode
var existing = list orelse return ; var existing = list orelse return;
var attributes = try self.arena.create(Element.Attribute.List); var attributes = try self.arena.create(Element.Attribute.List);
attributes.* = .{}; attributes.* = .{};

View File

@@ -57,7 +57,7 @@
const firstScript = document.querySelector('script'); const firstScript = document.querySelector('script');
testing.expectEqual('SCRIPT', firstScript.tagName); testing.expectEqual('SCRIPT', firstScript.tagName);
testing.expectEqual(null, document.querySelector('select')); testing.expectEqual(null, document.querySelector('article'));
testing.expectEqual(null, document.querySelector('another')); testing.expectEqual(null, document.querySelector('another'));
} }
</script> </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); 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 { 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; const show = what_to_show orelse NodeFilter.SHOW_ALL;
return DOMNodeIterator.init(root, show, filter, page); return DOMNodeIterator.init(root, show, filter, page);

View File

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

View File

@@ -326,7 +326,7 @@ fn needsLowerCasing(name: []const u8) bool {
} }
pub const NamedNodeMap = struct { pub const NamedNodeMap = struct {
_list: List = .{}, _list: *List,
// Whenever the NamedNodeMap creates an Attribute, it needs to provide the // Whenever the NamedNodeMap creates an Attribute, it needs to provide the
// "ownerElement". // "ownerElement".
@@ -418,6 +418,12 @@ pub const InnerIterator = struct {
fn formatAttribute(name: []const u8, value: []const u8, writer: *std.Io.Writer) !void { fn formatAttribute(name: []const u8, value: []const u8, writer: *std.Io.Writer) !void {
try writer.writeAll(name); 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('='); try writer.writeByte('=');
if (value.len == 0) { if (value.len == 0) {
return writer.writeAll("\"\""); return writer.writeAll("\"\"");
@@ -433,6 +439,37 @@ fn formatAttribute(name: []const u8, value: []const u8, writer: *std.Io.Writer)
return writer.writeByte('"'); 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 { fn writeEscapedAttributeValue(value: []const u8, first_offset: usize, writer: *std.Io.Writer) !void {
// Write everything before the first special character // Write everything before the first special character
try writer.writeAll(value[0..first_offset]); try writer.writeAll(value[0..first_offset]);

View File

@@ -1,5 +1,8 @@
const std = @import("std"); const std = @import("std");
const log = @import("../../../log.zig");
const Http = @import("../../../http/Http.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
@@ -8,15 +11,64 @@ const Response = @import("Response.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
_arena: Allocator, const Fetch = @This();
_promise: js.Promise,
_has_response: bool, _page: *Page,
_response: std.ArrayList(u8),
_resolver: js.PersistentPromiseResolver,
pub const Input = Request.Input; pub const Input = Request.Input;
// @ZIGDOM just enough to get campire demo working
pub fn init(input: Input, page: *Page) !js.Promise { pub fn init(input: Input, page: *Page) !js.Promise {
// @ZIGDOM const request = try Request.init(input, page);
_ = input;
_ = page; const fetch = try page.arena.create(Fetch);
return undefined; 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| { ) catch |err| {
return page.js.rejectPromise(.{@errorName(err)}); return page.js.rejectPromise(.{@errorName(err)});
}; };
return page.js.resolvePromise(.{value}); return page.js.resolvePromise(value);
} }
pub const JsApi = struct { pub const JsApi = struct {

View File

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

View File

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

View File

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