From 67abc32c04ce952f80b74e062e0c5ad184daecd7 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 16 Sep 2025 12:41:50 -0400 Subject: [PATCH 01/11] Add support for building on iOS --- Makefile | 77 ++++++++++++++++++++++------------------ build.zig | 96 ++++++++++++++++++++++++++++++++++++-------------- build.zig.zon | 10 +++--- src/app.zig | 5 +++ src/server.zig | 13 +++++-- 5 files changed, 132 insertions(+), 69 deletions(-) diff --git a/Makefile b/Makefile index b0ae69015..77daf2a1a 100644 --- a/Makefile +++ b/Makefile @@ -9,24 +9,29 @@ F= # OS and ARCH kernel = $(shell uname -ms) ifeq ($(kernel), Darwin arm64) - OS := macos - ARCH := aarch64 + OS ?= macos + ARCH ?= aarch64 else ifeq ($(kernel), Darwin x86_64) - OS := macos - ARCH := x86_64 + OS ?= macos + ARCH ?= x86_64 else ifeq ($(kernel), Linux aarch64) - OS := linux - ARCH := aarch64 + OS ?= linux + ARCH ?= aarch64 else ifeq ($(kernel), Linux arm64) - OS := linux - ARCH := aarch64 + OS ?= linux + ARCH ?= aarch64 else ifeq ($(kernel), Linux x86_64) - OS := linux - ARCH := x86_64 + OS ?= linux + ARCH ?= x86_64 else $(error "Unhandled kernel: $(kernel)") endif +MAKE ?= make +CMAKE ?= cmake +AUTOCONF_FLAGS ?= +CFLAGS ?= +LDFLAGS ?= # Infos # ----- @@ -149,38 +154,40 @@ _install-netsurf: clean-netsurf mkdir -p $(BC_NS) && \ cp -R vendor/netsurf/share $(BC_NS) && \ export PREFIX=$(BC_NS) && \ - export OPTLDFLAGS="-L$(ICONV)/lib" && \ - export OPTCFLAGS="$(OPTCFLAGS) -I$(ICONV)/include" && \ + export OPTLDFLAGS="$(LDFLAGS) -L$(ICONV)/lib" && \ + export OPTCFLAGS="$(CFLAGS) $(OPTCFLAGS) -I$(ICONV)/include" && \ printf "\e[33mInstalling libwapcaplet...\e[0m\n" && \ cd vendor/netsurf/libwapcaplet && \ - BUILDDIR=$(BC_NS)/build/libwapcaplet make install && \ + BUILDDIR=$(BC_NS)/build/libwapcaplet $(MAKE) install && \ cd ../libparserutils && \ printf "\e[33mInstalling libparserutils...\e[0m\n" && \ - BUILDDIR=$(BC_NS)/build/libparserutils make install && \ + BUILDDIR=$(BC_NS)/build/libparserutils $(MAKE) install && \ cd ../libhubbub && \ printf "\e[33mInstalling libhubbub...\e[0m\n" && \ - BUILDDIR=$(BC_NS)/build/libhubbub make install && \ + BUILDDIR=$(BC_NS)/build/libhubbub $(MAKE) install && \ rm src/treebuilder/autogenerated-element-type.c && \ cd ../libdom && \ printf "\e[33mInstalling libdom...\e[0m\n" && \ - BUILDDIR=$(BC_NS)/build/libdom make install && \ - printf "\e[33mRunning libdom example...\e[0m\n" && \ - cd examples && \ - $(ZIG) cc \ - -I$(ICONV)/include \ - -I$(BC_NS)/include \ - -L$(ICONV)/lib \ - -L$(BC_NS)/lib \ - -liconv \ - -ldom \ - -lhubbub \ - -lparserutils \ - -lwapcaplet \ - -o a.out \ - dom-structure-dump.c \ - $(ICONV)/lib/libiconv.a && \ - ./a.out > /dev/null && \ - rm a.out && \ + BUILDDIR=$(BC_NS)/build/libdom $(MAKE) install && \ + if [ -z "$${SKIP_EXAMPLES}" ]; then \ + printf "\e[33mRunning libdom example...\e[0m\n" && \ + cd examples && \ + $(ZIG) cc \ + -I$(ICONV)/include \ + -I$(BC_NS)/include \ + -L$(ICONV)/lib \ + -L$(BC_NS)/lib \ + -liconv \ + -ldom \ + -lhubbub \ + -lparserutils \ + -lwapcaplet \ + -o a.out \ + dom-structure-dump.c \ + $(ICONV)/lib/libiconv.a && \ + ./a.out > /dev/null && \ + rm a.out; \ + fi && \ printf "\e[36mDone NetSurf $(OS)\e[0m\n" clean-netsurf: @@ -204,7 +211,7 @@ endif build-libiconv: clean-libiconv @cd vendor/libiconv/libiconv-1.17 && \ - ./configure --prefix=$(ICONV) --enable-static && \ + ./configure --prefix=$(ICONV) --enable-static $(AUTOCONF_FLAGS) && \ make && make install install-libiconv: download-libiconv build-libiconv @@ -224,7 +231,7 @@ MIMALLOC := $(BC)vendor/mimalloc/out/$(OS)-$(ARCH) _build_mimalloc: clean-mimalloc @mkdir -p $(MIMALLOC)/build && \ cd $(MIMALLOC)/build && \ - cmake -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) ../../.. && \ + $(CMAKE) -DMI_BUILD_SHARED=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF -DMI_OVERRIDE=OFF $(OPTS) ../../.. && \ make && \ mkdir -p $(MIMALLOC)/lib diff --git a/build.zig b/build.zig index 1c9385818..78ca83f5e 100644 --- a/build.zig +++ b/build.zig @@ -62,28 +62,36 @@ pub fn build(b: *Build) !void { try addDependencies(b, lightpanda_module, opts); { - // browser - // ------- - - // compile and install - const exe = b.addExecutable(.{ - .name = "lightpanda", - .use_llvm = true, - .root_module = lightpanda_module, - }); - b.installArtifact(exe); + // static lib + // ---------- - // run - const run_cmd = b.addRunArtifact(exe); - if (b.args) |args| { - run_cmd.addArgs(args); - } - - // step - const run_step = b.step("run", "Run the app"); - run_step.dependOn(&run_cmd.step); + const lib = b.addLibrary(.{ .name = "lightpanda", .root_module = lightpanda_module, .use_llvm = true, .linkage = .static }); + b.installArtifact(lib); } + // { + // // browser + // // ------- + + // // compile and install + // const exe = b.addExecutable(.{ + // .name = "lightpanda", + // .use_llvm = true, + // .root_module = lightpanda_module, + // }); + // b.installArtifact(exe); + + // // run + // const run_cmd = b.addRunArtifact(exe); + // if (b.args) |args| { + // run_cmd.addArgs(args); + // } + + // // step + // const run_step = b.step("run", "Run the app"); + // run_step.dependOn(&run_cmd.step); + // } + { // tests // ---- @@ -176,6 +184,7 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo const os = switch (target.result.os.tag) { .linux => "linux", .macos => "macos", + .ios => "ios", else => return error.UnsupportedPlatform, }; var lib_path = try std.fmt.allocPrint( @@ -199,6 +208,12 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo mod.addSystemFrameworkPath(.{ .cwd_relative = "/System/Library/Frameworks" }); mod.linkFramework("CoreFoundation", .{}); }, + .ios => { + const sdk_path = try std.process.getEnvVarOwned(mod.owner.allocator, "SDK"); + const framework_path = try std.fmt.allocPrint(mod.owner.allocator, "{s}/System/Library/Frameworks", .{sdk_path}); + mod.addSystemFrameworkPath(.{ .cwd_relative = framework_path }); + mod.linkFramework("CoreFoundation", .{}); + }, else => {}, } } @@ -390,6 +405,13 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo mod.linkFramework("CoreFoundation", .{}); mod.linkFramework("SystemConfiguration", .{}); }, + .ios => { + const sdk_path = try std.process.getEnvVarOwned(mod.owner.allocator, "SDK"); + const framework_path = try std.fmt.allocPrint(mod.owner.allocator, "{s}/System/Library/Frameworks", .{sdk_path}); + mod.addSystemFrameworkPath(.{ .cwd_relative = framework_path }); + mod.linkFramework("CoreFoundation", .{}); + mod.linkFramework("SystemConfiguration", .{}); + }, else => {}, } } @@ -397,19 +419,33 @@ fn addDependencies(b: *Build, mod: *Build.Module, opts: *Build.Step.Options) !vo fn moduleNetSurf(b: *Build, mod: *Build.Module) !void { const target = mod.resolved_target.?; - const os = target.result.os.tag; - const arch = target.result.cpu.arch; + const os = switch (target.result.os.tag) { + .linux => "linux", + .macos => "macos", + .ios => switch (target.result.abi) { + .simulator => "iphonesimulator", + else => return error.UnsupportedPlatform, + }, + else => return error.UnsupportedPlatform, + }; + const arch = switch (target.result.os.tag) { + .ios => switch (target.result.cpu.arch) { + .aarch64 => "arm64", + else => @tagName(target.result.cpu.arch), + }, + else => @tagName(target.result.cpu.arch), + }; // iconv const libiconv_lib_path = try std.fmt.allocPrint( b.allocator, "vendor/libiconv/out/{s}-{s}/lib/libiconv.a", - .{ @tagName(os), @tagName(arch) }, + .{ os, arch }, ); const libiconv_include_path = try std.fmt.allocPrint( b.allocator, "vendor/libiconv/out/{s}-{s}/lib/libiconv.a", - .{ @tagName(os), @tagName(arch) }, + .{ os, arch }, ); mod.addObjectFile(b.path(libiconv_lib_path)); mod.addIncludePath(b.path(libiconv_include_path)); @@ -420,7 +456,7 @@ fn moduleNetSurf(b: *Build, mod: *Build.Module) !void { const lib_path = try std.fmt.allocPrint( b.allocator, mimalloc ++ "/out/{s}-{s}/lib/libmimalloc.a", - .{ @tagName(os), @tagName(arch) }, + .{ os, arch }, ); mod.addObjectFile(b.path(lib_path)); mod.addIncludePath(b.path(mimalloc ++ "/include")); @@ -431,7 +467,7 @@ fn moduleNetSurf(b: *Build, mod: *Build.Module) !void { const ns_include_path = try std.fmt.allocPrint( b.allocator, ns ++ "/out/{s}-{s}/include", - .{ @tagName(os), @tagName(arch) }, + .{ os, arch }, ); mod.addIncludePath(b.path(ns_include_path)); @@ -445,7 +481,7 @@ fn moduleNetSurf(b: *Build, mod: *Build.Module) !void { const ns_lib_path = try std.fmt.allocPrint( b.allocator, ns ++ "/out/{s}-{s}/lib/" ++ lib ++ ".a", - .{ @tagName(os), @tagName(arch) }, + .{ os, arch }, ); mod.addObjectFile(b.path(ns_lib_path)); mod.addIncludePath(b.path(ns ++ "/" ++ lib ++ "/src")); @@ -494,7 +530,7 @@ fn buildMbedtls(b: *Build, m: *Build.Module) !void { mbedtls.addIncludePath(b.path(root ++ "include")); mbedtls.addIncludePath(b.path(root ++ "library")); - mbedtls.addCSourceFiles(.{ .flags = &.{}, .files = &.{ + mbedtls.addCSourceFiles(.{ .flags = &.{"-Wno-nullability-completeness"}, .files = &.{ root ++ "library/aes.c", root ++ "library/aesni.c", root ++ "library/aesce.c", @@ -648,6 +684,12 @@ fn buildNghttp2(b: *Build, m: *Build.Module) !void { } fn buildCurl(b: *Build, m: *Build.Module) !void { + if (m.resolved_target.?.result.os.tag == .ios) { + const sdk_path = try std.process.getEnvVarOwned(b.allocator, "SDK"); + const include_path = try std.fmt.allocPrint(b.allocator, "{s}/usr/include", .{sdk_path}); + m.addIncludePath(.{ .cwd_relative = include_path }); + } + const curl = b.addLibrary(.{ .name = "curl", .root_module = m, diff --git a/build.zig.zon b/build.zig.zon index ff74c5619..e60460820 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,10 +4,10 @@ .version = "0.0.0", .fingerprint = 0xda130f3af836cea0, .dependencies = .{ - .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/7177ee1ae267a44751a0e7e012e257177699a375.tar.gz", - .hash = "v8-0.0.0-xddH63TCAwC1D1hEiOtbEnLBbtz9ZPHrdiGWLcBcYQB7", - }, - // .v8 = .{ .path = "../zig-v8-fork" } + // .v8 = .{ + // .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/7177ee1ae267a44751a0e7e012e257177699a375.tar.gz", + // .hash = "v8-0.0.0-xddH63TCAwC1D1hEiOtbEnLBbtz9ZPHrdiGWLcBcYQB7", + // }, + .v8 = .{ .path = "../zig-v8-fork" }, }, } diff --git a/src/app.zig b/src/app.zig index ee690e731..afb76cf5b 100644 --- a/src/app.zig +++ b/src/app.zig @@ -96,6 +96,11 @@ fn getAndMakeAppDir(allocator: Allocator) ?[]const u8 { if (@import("builtin").is_test) { return allocator.dupe(u8, "/tmp") catch unreachable; } + + if (@import("builtin").os.tag == .ios) { + return null; // getAppDataDir is not available on iOS + } + const app_dir_path = std.fs.getAppDataDir(allocator, "lightpanda") catch |err| { log.warn(.app, "get data dir", .{ .err = err }); return null; diff --git a/src/server.zig b/src/server.zig index f4f169d11..0f7badb14 100644 --- a/src/server.zig +++ b/src/server.zig @@ -77,8 +77,17 @@ pub const Server = struct { self.listener = listener; try posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1))); - if (@hasDecl(posix.TCP, "NODELAY")) { - try posix.setsockopt(listener, posix.IPPROTO.TCP, posix.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1))); + switch (builtin.os.tag) { + .ios => { + // TCP.NODELAY is not defined for iOS in posix module + const TCP_NODELAY = 0x01; + try posix.setsockopt(listener, posix.IPPROTO.TCP, TCP_NODELAY, &std.mem.toBytes(@as(c_int, 1))); + }, + else => { + if (@hasDecl(posix.TCP, "NODELAY")) { + try posix.setsockopt(listener, posix.IPPROTO.TCP, posix.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1))); + } + }, } try posix.bind(listener, &address.any, address.getOsSockLen()); From e575313830455ada9837b3caf1f753bbcc8d4ba1 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 16 Sep 2025 13:26:30 -0400 Subject: [PATCH 02/11] Remove static lib --- build.zig | 46 +++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/build.zig b/build.zig index 78ca83f5e..7074d3c75 100644 --- a/build.zig +++ b/build.zig @@ -62,35 +62,27 @@ pub fn build(b: *Build) !void { try addDependencies(b, lightpanda_module, opts); { - // static lib - // ---------- + // browser + // ------- - const lib = b.addLibrary(.{ .name = "lightpanda", .root_module = lightpanda_module, .use_llvm = true, .linkage = .static }); - b.installArtifact(lib); - } + // compile and install + const exe = b.addExecutable(.{ + .name = "lightpanda", + .use_llvm = true, + .root_module = lightpanda_module, + }); + b.installArtifact(exe); - // { - // // browser - // // ------- - - // // compile and install - // const exe = b.addExecutable(.{ - // .name = "lightpanda", - // .use_llvm = true, - // .root_module = lightpanda_module, - // }); - // b.installArtifact(exe); - - // // run - // const run_cmd = b.addRunArtifact(exe); - // if (b.args) |args| { - // run_cmd.addArgs(args); - // } - - // // step - // const run_step = b.step("run", "Run the app"); - // run_step.dependOn(&run_cmd.step); - // } + // run + const run_cmd = b.addRunArtifact(exe); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // step + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + } { // tests From b5bf9674dff0fb055d1ea6a094cb20e50aec638e Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 16 Sep 2025 13:28:38 -0400 Subject: [PATCH 03/11] Use upstream v8 --- build.zig.zon | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index e60460820..340d4e94f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,10 +4,10 @@ .version = "0.0.0", .fingerprint = 0xda130f3af836cea0, .dependencies = .{ - // .v8 = .{ - // .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/7177ee1ae267a44751a0e7e012e257177699a375.tar.gz", - // .hash = "v8-0.0.0-xddH63TCAwC1D1hEiOtbEnLBbtz9ZPHrdiGWLcBcYQB7", - // }, - .v8 = .{ .path = "../zig-v8-fork" }, + .v8 = .{ + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/7177ee1ae267a44751a0e7e012e257177699a375.tar.gz", + .hash = "v8-0.0.0-xddH63TCAwC1D1hEiOtbEnLBbtz9ZPHrdiGWLcBcYQB7", + }, + // .v8 = .{ .path = "../zig-v8-fork" }, }, } From b40306ec449d66d049d58f5c53b90518bf4d4b8d Mon Sep 17 00:00:00 2001 From: Isaac Yonemoto Date: Thu, 11 Sep 2025 11:14:25 -0500 Subject: [PATCH 04/11] modifies xhr to transfer cookies in toto --- src/browser/xhr/xhr.zig | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index 3117e9e9b..387cd7d9c 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -374,6 +374,10 @@ pub const XMLHttpRequest = struct { for (self.headers.items) |hdr| { try headers.add(hdr); } + + // transfer cookies to headers + addCookies(&headers, page.cookie_jar, page.arena); + try page.requestCookie(.{}).headersForRequest(self.arena, self.url.?, &headers); try page.http_client.request(.{ @@ -392,6 +396,27 @@ pub const XMLHttpRequest = struct { }); } + fn addCookies(headers: *Http.Headers, jar: *CookieJar, allocator: Allocator) void { + var cookiebuf: [4093:0]u8 = undefined; + var write_head: [*]u8 = &cookiebuf; + for (jar.cookies.items) |cookie| { + write_head = writeStr(write_head, cookie.name); + write_head = writeStr(write_head, "="); + write_head = writeStr(write_head, cookie.value); + write_head = writeStr(write_head, ";"); + } + write_head[0] = 0; + const len = write_head - &cookiebuf; + const cookiestr = allocator.allocSentinel(u8, len, 0) catch return; + @memmove(cookiestr, cookiebuf[0..len]); + headers.cookies = cookiestr.ptr; + } + + fn writeStr(dest: [*]u8, src: []const u8) [*]u8 { + @memmove(dest[0..src.len], src); + return dest + src.len; + } + fn httpStartCallback(transfer: *Http.Transfer) !void { const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx)); log.debug(.http, "request start", .{ .method = self.method, .url = self.url, .source = "xhr" }); From 8c0dac2bab749ba68a2fc2ccdc9640d9d95aa5df Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 16 Sep 2025 16:57:41 -0400 Subject: [PATCH 05/11] Add API for calling CDP from Swift --- build.zig | 52 ++++++++++------ build.zig.zon | 10 +-- include/lightpanda.h | 22 +++++++ include/module.modulemap | 4 ++ src/lib.zig | 129 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 195 insertions(+), 22 deletions(-) create mode 100644 include/lightpanda.h create mode 100644 include/module.modulemap create mode 100644 src/lib.zig diff --git a/build.zig b/build.zig index 7074d3c75..cdcd71d81 100644 --- a/build.zig +++ b/build.zig @@ -62,28 +62,46 @@ pub fn build(b: *Build) !void { try addDependencies(b, lightpanda_module, opts); { - // browser - // ------- + // static lib + // ---------- - // compile and install - const exe = b.addExecutable(.{ - .name = "lightpanda", - .use_llvm = true, - .root_module = lightpanda_module, + const liblightpanda_module = b.addModule("lightpanda", .{ + .root_source_file = b.path("src/lib.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + .link_libcpp = true, }); - b.installArtifact(exe); + try addDependencies(b, liblightpanda_module, opts); - // run - const run_cmd = b.addRunArtifact(exe); - if (b.args) |args| { - run_cmd.addArgs(args); - } - - // step - const run_step = b.step("run", "Run the app"); - run_step.dependOn(&run_cmd.step); + const lib = b.addLibrary(.{ .name = "lightpanda", .root_module = liblightpanda_module, .use_llvm = true, .linkage = .static }); + lib.bundle_compiler_rt = true; + b.installArtifact(lib); } + // { + // // browser + // // ------- + + // // compile and install + // const exe = b.addExecutable(.{ + // .name = "lightpanda", + // .use_llvm = true, + // .root_module = lightpanda_module, + // }); + // b.installArtifact(exe); + + // // run + // const run_cmd = b.addRunArtifact(exe); + // if (b.args) |args| { + // run_cmd.addArgs(args); + // } + + // // step + // const run_step = b.step("run", "Run the app"); + // run_step.dependOn(&run_cmd.step); + // } + { // tests // ---- diff --git a/build.zig.zon b/build.zig.zon index 340d4e94f..e60460820 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,10 +4,10 @@ .version = "0.0.0", .fingerprint = 0xda130f3af836cea0, .dependencies = .{ - .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/7177ee1ae267a44751a0e7e012e257177699a375.tar.gz", - .hash = "v8-0.0.0-xddH63TCAwC1D1hEiOtbEnLBbtz9ZPHrdiGWLcBcYQB7", - }, - // .v8 = .{ .path = "../zig-v8-fork" }, + // .v8 = .{ + // .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/7177ee1ae267a44751a0e7e012e257177699a375.tar.gz", + // .hash = "v8-0.0.0-xddH63TCAwC1D1hEiOtbEnLBbtz9ZPHrdiGWLcBcYQB7", + // }, + .v8 = .{ .path = "../zig-v8-fork" }, }, } diff --git a/include/lightpanda.h b/include/lightpanda.h new file mode 100644 index 000000000..318a2ed3a --- /dev/null +++ b/include/lightpanda.h @@ -0,0 +1,22 @@ +#ifndef _LIGHTPANDA_H +#define _LIGHTPANDA_H + +void* lightpanda_app_init(); +void lightpanda_app_deinit(void* app_ptr); + +void* lightpanda_browser_init(void* app_ptr); +void lightpanda_browser_deinit(void* browser_ptr); + +void* lightpanda_browser_new_session(void* browser_ptr); + +void* lightpanda_session_create_page(void* session_ptr); + +void lightpanda_page_navigate(void* page_ptr, const char *url); + +void* lightpanda_cdp_init(void* app_ptr, void (*handler_fn)(void*, const char *), void* ctx); +void lightpanda_cdp_deinit(void* cdp_ptr); +const char* lightpanda_cdp_create_browser_context(void* cdp_ptr); +void* lightpanda_cdp_browser(void* cdp_ptr); +void lightpanda_cdp_process_message(void* cdp_ptr, const char *msg); + +#endif \ No newline at end of file diff --git a/include/module.modulemap b/include/module.modulemap new file mode 100644 index 000000000..c95aa7596 --- /dev/null +++ b/include/module.modulemap @@ -0,0 +1,4 @@ +module lightpanda { + umbrella header "lightpanda.h" + export * +} \ No newline at end of file diff --git a/src/lib.zig b/src/lib.zig new file mode 100644 index 000000000..79e8e96e1 --- /dev/null +++ b/src/lib.zig @@ -0,0 +1,129 @@ +const std = @import("std"); +const App = @import("app.zig").App; +const Browser = @import("browser/browser.zig").Browser; +const Session = @import("browser/session.zig").Session; +const Page = @import("browser/page.zig").Page; +const CDPT = @import("cdp/cdp.zig").CDPT; +const Command = @import("cdp/cdp.zig").Command; + +export fn lightpanda_app_init() ?*anyopaque { + const allocator = std.heap.c_allocator; + + const app = App.init(allocator, .{ .run_mode = .fetch, .tls_verify_host = false }) catch return null; + + return app; +} + +export fn lightpanda_app_deinit(app_ptr: *anyopaque) void { + const app: *App = @ptrCast(@alignCast(app_ptr)); + app.deinit(); +} + +export fn lightpanda_browser_init(app_ptr: *anyopaque) ?*anyopaque { + const app: *App = @ptrCast(@alignCast(app_ptr)); + + const browser = std.heap.c_allocator.create(Browser) catch return null; + browser.* = Browser.init(app) catch return null; + + return browser; +} + +export fn lightpanda_browser_deinit(browser_ptr: *anyopaque) void { + const browser: *Browser = @ptrCast(@alignCast(browser_ptr)); + browser.deinit(); +} + +export fn lightpanda_browser_new_session(browser_ptr: *anyopaque) ?*anyopaque { + const browser: *Browser = @ptrCast(@alignCast(browser_ptr)); + const session = browser.newSession() catch return null; + return session; +} + +export fn lightpanda_session_create_page(session_ptr: *anyopaque) ?*anyopaque { + const session: *Session = @ptrCast(@alignCast(session_ptr)); + const page = session.createPage() catch return null; + return page; +} + +export fn lightpanda_page_navigate(page_ptr: *anyopaque, url: [*:0]const u8) void { + const page: *Page = @ptrCast(@alignCast(page_ptr)); + page.navigate(std.mem.span(url), .{}) catch return; +} + +const NativeClientHandler = *const fn (ctx: *anyopaque, message: [*:0]const u8) callconv(.c) void; + +const NativeClient = struct { + allocator: std.mem.Allocator, + send_arena: std.heap.ArenaAllocator, + // sent: std.ArrayListUnmanaged(std.json.Value) = .{}, + // serialized: std.ArrayListUnmanaged([]const u8) = .{}, + handler: NativeClientHandler, + ctx: *anyopaque, + + fn init(alloc: std.mem.Allocator, handler: NativeClientHandler, ctx: *anyopaque) NativeClient { + return .{ .allocator = alloc, .send_arena = std.heap.ArenaAllocator.init(alloc), .handler = handler, .ctx = ctx }; + } + + pub fn sendJSON(self: *NativeClient, message: anytype, opts: std.json.Stringify.Options) !void { + var opts_copy = opts; + opts_copy.whitespace = .indent_2; + const serialized = try std.json.Stringify.valueAlloc(self.allocator, message, opts_copy); + + const slice = try self.allocator.dupeZ(u8, serialized); + defer self.allocator.free(slice); + self.handler(self.ctx, slice.ptr); + + // try self.serialized.append(self.allocator, serialized); + + // const value = try std.json.parseFromSliceLeaky(std.json.Value, self.allocator, serialized, .{}); + // try self.sent.append(self.allocator, value); + // @panic("trying to send JSON to nativeClient"); + } + + pub fn sendJSONRaw(self: *NativeClient, buf: std.ArrayListUnmanaged(u8)) !void { + // const value = try std.json.parseFromSliceLeaky(std.json.Value, self.allocator, buf.items, .{}); + // try self.sent.append(self.allocator, value); + // @panic("trying to send raw JSON to nativeClient"); + const slice = try self.allocator.dupeZ(u8, buf.items); + defer self.allocator.free(slice); + self.handler(self.ctx, slice.ptr); + } +}; + +const CDP = CDPT(struct { + pub const Client = *NativeClient; +}); + +export fn lightpanda_cdp_init(app_ptr: *anyopaque, handler: NativeClientHandler, ctx: *anyopaque) ?*anyopaque { + const app: *App = @ptrCast(@alignCast(app_ptr)); + + const client = app.allocator.create(NativeClient) catch return null; + client.* = NativeClient.init(app.allocator, handler, ctx); + + const cdp = app.allocator.create(CDP) catch return null; + cdp.* = CDP.init(app, client) catch return null; + + return cdp; +} + +export fn lightpanda_cdp_deinit(cdp_ptr: *anyopaque) void { + const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); + cdp.deinit(); +} + +export fn lightpanda_cdp_create_browser_context(cdp_ptr: *anyopaque) ?[*:0]const u8 { + const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); + const id = cdp.createBrowserContext() catch return null; + const slice = cdp.allocator.dupeZ(u8, id) catch return null; + return slice.ptr; +} + +export fn lightpanda_cdp_browser(cdp_ptr: *anyopaque) ?*anyopaque { + const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); + return &cdp.browser; +} + +export fn lightpanda_cdp_process_message(cdp_ptr: *anyopaque, msg: [*:0]const u8) void { + const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); + cdp.processMessage(std.mem.span(msg)) catch return; +} From 5a7af3966030915806b63399dd9bf1a1fa2d244d Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Tue, 16 Sep 2025 18:17:51 -0400 Subject: [PATCH 06/11] Add support for more CDP APIs --- include/lightpanda.h | 2 ++ src/cdp/domains/page.zig | 6 ++++-- src/lib.zig | 22 +++++++++++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/include/lightpanda.h b/include/lightpanda.h index 318a2ed3a..33b7b2e57 100644 --- a/include/lightpanda.h +++ b/include/lightpanda.h @@ -18,5 +18,7 @@ void lightpanda_cdp_deinit(void* cdp_ptr); const char* lightpanda_cdp_create_browser_context(void* cdp_ptr); void* lightpanda_cdp_browser(void* cdp_ptr); void lightpanda_cdp_process_message(void* cdp_ptr, const char *msg); +void* lightpanda_cdp_browser_context(void* cdp_ptr); +void* lightpanda_browser_context_session(void* browser_context_ptr); #endif \ No newline at end of file diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 4495a4745..1e5fde5b7 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -155,10 +155,12 @@ fn navigate(cmd: anytype) !void { var page = bc.session.currentPage() orelse return error.PageNotLoaded; bc.loader_id = bc.cdp.loader_id_gen.next(); - try page.navigate(params.url, .{ + page.navigate(params.url, .{ .reason = .address_bar, .cdp_id = cmd.input.id, - }); + }) catch |err| { + @import("std").debug.panic("navigate failed: {any}", .{err}); + }; } pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.PageNavigate) !void { diff --git a/src/lib.zig b/src/lib.zig index 79e8e96e1..4b5e1d250 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -4,7 +4,7 @@ const Browser = @import("browser/browser.zig").Browser; const Session = @import("browser/session.zig").Session; const Page = @import("browser/page.zig").Page; const CDPT = @import("cdp/cdp.zig").CDPT; -const Command = @import("cdp/cdp.zig").Command; +const BrowserContext = @import("cdp/cdp.zig").BrowserContext; export fn lightpanda_app_init() ?*anyopaque { const allocator = std.heap.c_allocator; @@ -114,6 +114,16 @@ export fn lightpanda_cdp_deinit(cdp_ptr: *anyopaque) void { export fn lightpanda_cdp_create_browser_context(cdp_ptr: *anyopaque) ?[*:0]const u8 { const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); const id = cdp.createBrowserContext() catch return null; + + _ = cdp.browser_context.?.session.createPage() catch return null; + + const target_id = cdp.target_id_gen.next(); + cdp.browser_context.?.target_id = target_id; + + const session_id = cdp.session_id_gen.next(); + cdp.browser_context.?.extra_headers.clearRetainingCapacity(); + cdp.browser_context.?.session_id = session_id; + const slice = cdp.allocator.dupeZ(u8, id) catch return null; return slice.ptr; } @@ -127,3 +137,13 @@ export fn lightpanda_cdp_process_message(cdp_ptr: *anyopaque, msg: [*:0]const u8 const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); cdp.processMessage(std.mem.span(msg)) catch return; } + +export fn lightpanda_cdp_browser_context(cdp_ptr: *anyopaque) *anyopaque { + const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); + return &cdp.browser_context.?; +} + +export fn lightpanda_browser_context_session(browser_context_ptr: *anyopaque) *anyopaque { + const browser_context: *BrowserContext(CDP) = @ptrCast(@alignCast(browser_context_ptr)); + return browser_context.session; +} From 25ed528432f0c8e47034e6487b07e4f6c339c956 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Thu, 18 Sep 2025 12:21:38 -0400 Subject: [PATCH 07/11] Expose APIs for pageWait --- include/lightpanda.h | 9 +++++++++ src/lib.zig | 36 +++++++++++++++++++++++++----------- src/log.zig | 2 +- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/include/lightpanda.h b/include/lightpanda.h index 33b7b2e57..fb9585d3b 100644 --- a/include/lightpanda.h +++ b/include/lightpanda.h @@ -1,6 +1,12 @@ #ifndef _LIGHTPANDA_H #define _LIGHTPANDA_H +typedef enum { + done = 0, + no_page = 1, + extra_socket = 2, +} Session_WaitResult; + void* lightpanda_app_init(); void lightpanda_app_deinit(void* app_ptr); @@ -10,6 +16,7 @@ void lightpanda_browser_deinit(void* browser_ptr); void* lightpanda_browser_new_session(void* browser_ptr); void* lightpanda_session_create_page(void* session_ptr); +void* lightpanda_session_page(void* session_ptr); void lightpanda_page_navigate(void* page_ptr, const char *url); @@ -19,6 +26,8 @@ const char* lightpanda_cdp_create_browser_context(void* cdp_ptr); void* lightpanda_cdp_browser(void* cdp_ptr); void lightpanda_cdp_process_message(void* cdp_ptr, const char *msg); void* lightpanda_cdp_browser_context(void* cdp_ptr); +Session_WaitResult lightpanda_cdp_page_wait(void* cdp_ptr, int ms); + void* lightpanda_browser_context_session(void* browser_context_ptr); #endif \ No newline at end of file diff --git a/src/lib.zig b/src/lib.zig index 4b5e1d250..93f9f86ed 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -9,7 +9,19 @@ const BrowserContext = @import("cdp/cdp.zig").BrowserContext; export fn lightpanda_app_init() ?*anyopaque { const allocator = std.heap.c_allocator; - const app = App.init(allocator, .{ .run_mode = .fetch, .tls_verify_host = false }) catch return null; + const app = App.init(allocator, .{ + // .run_mode = .serve, + // .tls_verify_host = false + .run_mode = .serve, + .tls_verify_host = false, + // .http_proxy = null, + // .proxy_bearer_token = args.proxyBearerToken(), + // .tls_verify_host = args.tlsVerifyHost(), + // .http_timeout_ms = args.httpTimeout(), + // .http_connect_timeout_ms = args.httpConnectTiemout(), + // .http_max_host_open = args.httpMaxHostOpen(), + // .http_max_concurrent = args.httpMaxConcurrent(), + }) catch return null; return app; } @@ -45,6 +57,11 @@ export fn lightpanda_session_create_page(session_ptr: *anyopaque) ?*anyopaque { return page; } +export fn lightpanda_session_page(session_ptr: *anyopaque) ?*anyopaque { + const session: *Session = @ptrCast(@alignCast(session_ptr)); + return &session.page; +} + export fn lightpanda_page_navigate(page_ptr: *anyopaque, url: [*:0]const u8) void { const page: *Page = @ptrCast(@alignCast(page_ptr)); page.navigate(std.mem.span(url), .{}) catch return; @@ -72,19 +89,11 @@ const NativeClient = struct { const slice = try self.allocator.dupeZ(u8, serialized); defer self.allocator.free(slice); self.handler(self.ctx, slice.ptr); - - // try self.serialized.append(self.allocator, serialized); - - // const value = try std.json.parseFromSliceLeaky(std.json.Value, self.allocator, serialized, .{}); - // try self.sent.append(self.allocator, value); - // @panic("trying to send JSON to nativeClient"); } pub fn sendJSONRaw(self: *NativeClient, buf: std.ArrayListUnmanaged(u8)) !void { - // const value = try std.json.parseFromSliceLeaky(std.json.Value, self.allocator, buf.items, .{}); - // try self.sent.append(self.allocator, value); - // @panic("trying to send raw JSON to nativeClient"); - const slice = try self.allocator.dupeZ(u8, buf.items); + const msg = buf.items[10..]; // CDP adds 10 0s for a WebSocket header. + const slice = try self.allocator.dupeZ(u8, msg); defer self.allocator.free(slice); self.handler(self.ctx, slice.ptr); } @@ -143,6 +152,11 @@ export fn lightpanda_cdp_browser_context(cdp_ptr: *anyopaque) *anyopaque { return &cdp.browser_context.?; } +export fn lightpanda_cdp_page_wait(cdp_ptr: *anyopaque, ms: i32) c_int { + const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); + return @intFromEnum(cdp.pageWait(ms)); +} + export fn lightpanda_browser_context_session(browser_context_ptr: *anyopaque) *anyopaque { const browser_context: *BrowserContext(CDP) = @ptrCast(@alignCast(browser_context_ptr)); return browser_context.session; diff --git a/src/log.zig b/src/log.zig index 24ae9ff89..9a26c2a8c 100644 --- a/src/log.zig +++ b/src/log.zig @@ -49,7 +49,7 @@ const Opts = struct { filter_scopes: []const Scope = &.{}, }; -pub var opts = Opts{}; +pub var opts = Opts{ .level = .info, .format = .pretty }; // synchronizes writes to the output var out_lock: Thread.Mutex = .{}; From 587f5277358a52f197cd802e06f0ec79ed90c231 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Thu, 18 Sep 2025 15:25:25 -0400 Subject: [PATCH 08/11] Add get_firstElementChild to DocumentFragment --- src/.DS_Store | Bin 0 -> 6148 bytes src/browser/dom/document_fragment.zig | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 src/.DS_Store diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..dc0d75e74f258ffeebbf97fbd9d5967eb554ae0f GIT binary patch literal 6148 zcmeHKu}T9$5S{fBjS8Zjg2mYgViiawDrbyeU?bRQOfW*u3rR!-iv#@vLD9}n@B_rg z!p25RKfqtG_08^j?&dCHqloOl?%V9l?0Y-O?dFI`ln-h}qH!WBpfTn~&}A6=xutAC z&rAb_T%$vEYBzU_>pM{@+D*6$xC;EW3h=W#L{(~2M2BQNzhO&C9XsDoM}$$OQf)?6 zcnX_SxA#L!4=Y(wwQoeV8hl!4wuulA7O(KJG`{aP9ne0Q_b8;gwQKM6DIH;I7<^3j-m{LWq<@s9$Gc%Y z!%$MECK}XW+@>Q-6BVzrqfA{#uZgaV^-OJFM_T$QV10S1uY5Itj^)bu_ckZzw{tW8RnHBm#x5cNK6I zuq)uHkK=s*ul0Zbx0Bo_R{>Xne^mjItE^Yb*pj|mQ=8+v)<@eyW8=73p-e%iuVXpj et9TMk8hkzvfPuzXA!cCikAN(L>s$qXwF2+aCE!^A literal 0 HcmV?d00001 diff --git a/src/browser/dom/document_fragment.zig b/src/browser/dom/document_fragment.zig index 7841d4668..a38302315 100644 --- a/src/browser/dom/document_fragment.zig +++ b/src/browser/dom/document_fragment.zig @@ -88,6 +88,11 @@ pub const DocumentFragment = struct { const e = try parser.nodeGetElementById(@ptrCast(@alignCast(self)), id) orelse return null; return try Element.toInterface(e); } + + pub fn get_firstElementChild(self: *parser.DocumentFragment) !?ElementUnion { + var children = try get_children(self); + return try children._item(0); + } }; const testing = @import("../../testing.zig"); From f362601788a01c0f03ffc5f2b0c8d6d4aa6f2da7 Mon Sep 17 00:00:00 2001 From: Isaac Yonemoto Date: Thu, 18 Sep 2025 17:13:09 -0500 Subject: [PATCH 09/11] makes it so lightpanda_cdp_page_wait emits a number, which is the peeked number of milliseconds until the next event is required --- include/lightpanda.h | 8 +------- src/lib.zig | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/include/lightpanda.h b/include/lightpanda.h index fb9585d3b..a18df3dec 100644 --- a/include/lightpanda.h +++ b/include/lightpanda.h @@ -1,12 +1,6 @@ #ifndef _LIGHTPANDA_H #define _LIGHTPANDA_H -typedef enum { - done = 0, - no_page = 1, - extra_socket = 2, -} Session_WaitResult; - void* lightpanda_app_init(); void lightpanda_app_deinit(void* app_ptr); @@ -26,7 +20,7 @@ const char* lightpanda_cdp_create_browser_context(void* cdp_ptr); void* lightpanda_cdp_browser(void* cdp_ptr); void lightpanda_cdp_process_message(void* cdp_ptr, const char *msg); void* lightpanda_cdp_browser_context(void* cdp_ptr); -Session_WaitResult lightpanda_cdp_page_wait(void* cdp_ptr, int ms); +int lightpanda_cdp_page_wait(void* cdp_ptr, int ms); void* lightpanda_browser_context_session(void* browser_context_ptr); diff --git a/src/lib.zig b/src/lib.zig index 93f9f86ed..231d65422 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -3,6 +3,7 @@ const App = @import("app.zig").App; const Browser = @import("browser/browser.zig").Browser; const Session = @import("browser/session.zig").Session; const Page = @import("browser/page.zig").Page; +const Scheduler = @import("browser/Scheduler.zig"); const CDPT = @import("cdp/cdp.zig").CDPT; const BrowserContext = @import("cdp/cdp.zig").BrowserContext; @@ -152,9 +153,27 @@ export fn lightpanda_cdp_browser_context(cdp_ptr: *anyopaque) *anyopaque { return &cdp.browser_context.?; } +// returns -1 if no session/page, or if no events reamin, otherwise returns +// milliseconds until next scheduled task export fn lightpanda_cdp_page_wait(cdp_ptr: *anyopaque, ms: i32) c_int { const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); - return @intFromEnum(cdp.pageWait(ms)); + _ = cdp.pageWait(ms); + + // it's okay to panic if the session or page don't exist. + const scheduler = &cdp.browser.session.?.page.?.scheduler; + return cdp_peek_next_delay_ms(scheduler) orelse -1; +} + +fn cdp_peek_next_delay_ms(scheduler: *Scheduler) ?i32 { + if (scheduler.high_priority.count() == 0) { + return null; + } + + const now = std.time.milliTimestamp(); + const next_task = scheduler.high_priority.peek().?; + const time_to_next = next_task.ms - now; + + return if (time_to_next > 0) @intCast(time_to_next) else 0; } export fn lightpanda_browser_context_session(browser_context_ptr: *anyopaque) *anyopaque { From c305858ee3a1faa724d91259174c05e87c739570 Mon Sep 17 00:00:00 2001 From: Isaac Yonemoto Date: Fri, 19 Sep 2025 08:52:18 -0500 Subject: [PATCH 10/11] checks the low priority queue for timings and enforces task flush on connection wait --- src/browser/page.zig | 3 +++ src/lib.zig | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/browser/page.zig b/src/browser/page.zig index ae426d21d..fbd19a3e3 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -394,6 +394,9 @@ pub const Page = struct { return err; }, .raw_done => { + // Run scheduler to clean up any pending tasks + _ = try scheduler.run(); + if (exit_when_done) { return .done; } diff --git a/src/lib.zig b/src/lib.zig index 231d65422..c6a5de0ba 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -153,7 +153,7 @@ export fn lightpanda_cdp_browser_context(cdp_ptr: *anyopaque) *anyopaque { return &cdp.browser_context.?; } -// returns -1 if no session/page, or if no events reamin, otherwise returns +// returns -1 if no session/page, or if no events reamin, otherwise returns // milliseconds until next scheduled task export fn lightpanda_cdp_page_wait(cdp_ptr: *anyopaque, ms: i32) c_int { const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); @@ -165,14 +165,20 @@ export fn lightpanda_cdp_page_wait(cdp_ptr: *anyopaque, ms: i32) c_int { } fn cdp_peek_next_delay_ms(scheduler: *Scheduler) ?i32 { - if (scheduler.high_priority.count() == 0) { - return null; - } + var queue = queue: { + if (scheduler.high_priority.count() == 0) { + if (scheduler.low_priority.count() == 0) return null; + break :queue scheduler.low_priority; + } else { + break :queue scheduler.high_priority; + } + }; const now = std.time.milliTimestamp(); - const next_task = scheduler.high_priority.peek().?; - const time_to_next = next_task.ms - now; + // we know this must exist because the count was not 0. + const next_task = queue.peek().?; + const time_to_next = next_task.ms - now; return if (time_to_next > 0) @intCast(time_to_next) else 0; } From 595f1a5f91e46982a99fbd37141dabd60f854807 Mon Sep 17 00:00:00 2001 From: Isaac Yonemoto Date: Fri, 19 Sep 2025 19:19:36 -0500 Subject: [PATCH 11/11] implements DOM.attributeModified --- src/browser/netsurf.zig | 8 ++++ src/browser/page.zig | 1 + src/cdp/domains/page.zig | 81 +++++++++++++++++++++++++++++++++++++++- src/lib.zig | 6 ++- 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index aa20be606..8ecad1a02 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -579,6 +579,14 @@ pub fn mutationEventPrevValue(evt: *MutationEvent) !?[]const u8 { return strToData(s.?); } +pub fn mutationEventNewValue(evt: *MutationEvent) !?[]const u8 { + var s: ?*String = null; + const err = c._dom_mutation_event_get_new_value(evt, &s); + try DOMErr(err); + if (s == null) return null; + return strToData(s.?); +} + pub fn mutationEventRelatedNode(evt: *MutationEvent) !?*Node { var n: NodeExternal = undefined; const err = c._dom_mutation_event_get_related_node(evt, &n); diff --git a/src/browser/page.zig b/src/browser/page.zig index fbd19a3e3..3d4902d6d 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -92,6 +92,7 @@ pub const Page = struct { notified_network_idle: IdleNotification = .init, notified_network_almost_idle: IdleNotification = .init, + auto_enable_dom_monitoring: bool = false, const Mode = union(enum) { pre: void, diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 1e5fde5b7..57e1b6600 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -20,6 +20,8 @@ const std = @import("std"); const URL = @import("../../url.zig").URL; const Page = @import("../../browser/page.zig").Page; const Notification = @import("../../notification.zig").Notification; +const log = @import("../../log.zig"); +const parser = @import("../../browser/netsurf.zig"); const Allocator = std.mem.Allocator; @@ -354,9 +356,20 @@ pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !voi } // frameStoppedLoading - return cdp.sendEvent("Page.frameStoppedLoading", .{ + try cdp.sendEvent("Page.frameStoppedLoading", .{ .frameId = target_id, }, .{ .session_id = session_id }); + + // Auto-enable DOM monitoring after page navigation is complete + const page = bc.session.currentPage() orelse return; + if (page.auto_enable_dom_monitoring) { + std.debug.print("dom monitoring enabled\n", .{}); + autoEnableDOMMonitoring(bc, page) catch |err| { + log.warn(.cdp, "autoenable DOM monitor fail", .{.err = err}); + }; + } else { + std.debug.print("nalok\n", .{}); + } } pub fn pageNetworkIdle(bc: anytype, event: *const Notification.PageNetworkIdle) !void { @@ -389,6 +402,72 @@ const LifecycleEvent = struct { timestamp: u32, }; +// Auto-enable DOM monitoring when pages are created/navigated +fn autoEnableDOMMonitoring(bc: anytype, page: anytype) !void { + const BC = @TypeOf(bc.*); + const CDP = @TypeOf(bc.cdp.*); + + // Check if we have a session (required for sending events) + const session_id = bc.session_id orelse return; + + // Create a CDP-specific mutation observer that sends DOM.attributeModified events + const arena = page.arena; + const doc = parser.documentHTMLToDocument(page.window.document); + const Observer = struct { + bc: *BC, + cdp: *CDP, + session_id: []const u8, + event_node: parser.EventNode, + + const Self = @This(); + + fn handle(en: *parser.EventNode, event: *parser.Event) void { + const self: *Self = @fieldParentPtr("event_node", en); + self._handle(event) catch |err| { + log.err(.cdp, "DOM Observer handle error", .{.err = err}); + }; + } + + fn _handle(self: *Self, event: *parser.Event) !void { + const mutation_event = parser.eventToMutationEvent(event); + const attribute_name = parser.mutationEventAttributeName(mutation_event) catch return; + const new_value = parser.mutationEventNewValue(mutation_event) catch null; + + // Get the target node and register it to get a CDP node ID + const event_target = parser.eventTarget(event) orelse return; + const target_node = parser.eventTargetToNode(event_target); + const node = try self.bc.node_registry.register(target_node); + + // Send CDP DOM.attributeModified event + try self.cdp.sendEvent("DOM.attributeModified", .{ + .nodeId = node.id, + .name = attribute_name, + .value = new_value, + }, .{ + .session_id = self.session_id, + }); + } + }; + + const cdp_observer = try arena.create(Observer); + cdp_observer.* = .{ + .bc = bc, + .cdp = bc.cdp, + .session_id = session_id, + .event_node = .{ .id = @intFromPtr(cdp_observer), .func = Observer.handle }, + }; + + // Add event listener to document element to catch all attribute changes + const document_element = (try parser.documentGetDocumentElement(doc)) orelse return; + + _ = try parser.eventTargetAddEventListener( + parser.toEventTarget(parser.Element, document_element), + "DOMAttrModified", + &cdp_observer.event_node, + true, // use capture to catch all events + ); +} + const testing = @import("../testing.zig"); test "cdp.page: getFrameTree" { var ctx = testing.context(); diff --git a/src/lib.zig b/src/lib.zig index c6a5de0ba..83f23ad38 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -10,6 +10,8 @@ const BrowserContext = @import("cdp/cdp.zig").BrowserContext; export fn lightpanda_app_init() ?*anyopaque { const allocator = std.heap.c_allocator; + @import("log.zig").opts.level = .warn; + const app = App.init(allocator, .{ // .run_mode = .serve, // .tls_verify_host = false @@ -55,6 +57,7 @@ export fn lightpanda_browser_new_session(browser_ptr: *anyopaque) ?*anyopaque { export fn lightpanda_session_create_page(session_ptr: *anyopaque) ?*anyopaque { const session: *Session = @ptrCast(@alignCast(session_ptr)); const page = session.createPage() catch return null; + page.auto_enable_dom_monitoring = true; return page; } @@ -125,7 +128,8 @@ export fn lightpanda_cdp_create_browser_context(cdp_ptr: *anyopaque) ?[*:0]const const cdp: *CDP = @ptrCast(@alignCast(cdp_ptr)); const id = cdp.createBrowserContext() catch return null; - _ = cdp.browser_context.?.session.createPage() catch return null; + const page = cdp.browser_context.?.session.createPage() catch return null; + page.auto_enable_dom_monitoring = true; const target_id = cdp.target_id_gen.next(); cdp.browser_context.?.target_id = target_id;