diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 9e4b8e38..25ce263b 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -13,7 +13,7 @@ inputs: zig-v8: description: 'zig v8 version to install' required: false - default: 'v0.2.5' + default: 'v0.2.6' v8: description: 'v8 version to install' required: false @@ -32,7 +32,7 @@ runs: shell: bash run: | sudo apt-get update - sudo apt-get install -y wget xz-utils python3 ca-certificates git pkg-config libglib2.0-dev gperf libexpat1-dev cmake clang + sudo apt-get install -y wget xz-utils ca-certificates gcc make git # Zig version used from the `minimum_zig_version` field in build.zig.zon - uses: mlugg/setup-zig@v2 diff --git a/Dockerfile b/Dockerfile index 5ee73356..dd8f7fac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,12 +3,12 @@ FROM debian:stable-slim ARG MINISIG=0.12 ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG V8=14.0.365.4 -ARG ZIG_V8=v0.2.5 +ARG ZIG_V8=v0.2.6 ARG TARGETPLATFORM RUN apt-get update -yq && \ apt-get install -yq xz-utils ca-certificates \ - clang make curl git + gcc make curl git # Get Rust RUN curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal -y diff --git a/README.md b/README.md index 478a28ed..b2b695f0 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ For **Debian/Ubuntu based Linux**: ``` sudo apt install xz-utils ca-certificates \ - clang make curl git + gcc make curl git ``` You also need to [install Rust](https://rust-lang.org/tools/install/). diff --git a/build.zig.zon b/build.zig.zon index f66a040e..f32868b8 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,8 +6,8 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/v0.2.5.tar.gz", - .hash = "v8-0.0.0-xddH641NBAC3MqKV44YCkwvnUenhQyGlgJI8OScx0tlP", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/v0.2.6.tar.gz", + .hash = "v8-0.0.0-xddH60NRBAAWmpZq9nWdfFAEqVJ9zqJnvr1Nl9m2AbcY", }, //.v8 = .{ .path = "../zig-v8-fork" }, .@"boringssl-zig" = .{ diff --git a/src/browser/Browser.zig b/src/browser/Browser.zig index ea39bcde..69af00a5 100644 --- a/src/browser/Browser.zig +++ b/src/browser/Browser.zig @@ -99,7 +99,7 @@ pub fn closeSession(self: *Browser) void { session.deinit(); self.session = null; _ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 }); - self.env.lowMemoryNotification(); + self.env.memoryPressureNotification(.critical); } } diff --git a/src/browser/Page.zig b/src/browser/Page.zig index ec87a8a2..fc7f353a 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -103,6 +103,20 @@ _element_shadow_roots: Element.ShadowRootLookup = .{}, _node_owner_documents: Node.OwnerDocumentLookup = .{}, _element_assigned_slots: Element.AssignedSlotLookup = .{}, +/// Lazily-created inline event listeners (or listeners provided as attributes). +/// Avoids bloating all elements with extra function fields for rare usage. +/// +/// Use this when a listener provided like these: +/// +/// ```html +/// +/// ``` +/// +/// ```js +/// img.onload = () => { ... }; +/// ``` +_element_attr_listeners: Element.AttrListenerLookup = .{}, + _script_manager: ScriptManager, // List of active MutationObservers @@ -175,7 +189,10 @@ call_arena: Allocator, arena_pool: *ArenaPool, // In Debug, we use this to see if anything fails to release an arena back to // the pool. -_arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, []const u8) else void), +_arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct { + owner: []const u8, + count: usize, +}) else void), window: *Window, document: *Document, @@ -237,7 +254,9 @@ pub fn deinit(self: *Page) void { if (comptime IS_DEBUG) { var it = self._arena_pool_leak_track.valueIterator(); while (it.next()) |value_ptr| { - log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* }); + if (value_ptr.count > 0) { + log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner }); + } } } @@ -245,28 +264,23 @@ pub fn deinit(self: *Page) void { } fn reset(self: *Page, comptime initializing: bool) !void { - if (comptime IS_DEBUG) { - var it = self._arena_pool_leak_track.valueIterator(); - while (it.next()) |value_ptr| { - log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* }); - } - self._arena_pool_leak_track.clearRetainingCapacity(); - } - if (comptime initializing == false) { - // Removins the context triggers the linked inspector. - // It seems to append a collect task to the message loop. self._session.executor.removeContext(); - // We force running the message loop after removing the context b/c we - // will force a GC run just after. If we remove this part, the task - // will run after the GC and we will use memory after free. - self._session.browser.runMessageLoop(); + // removing a context can trigger finalizers, so we can only check for + // a leak after the above. + if (comptime IS_DEBUG) { + var it = self._arena_pool_leak_track.valueIterator(); + while (it.next()) |value_ptr| { + log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* }); + } + self._arena_pool_leak_track.clearRetainingCapacity(); + } - // We force a garbage collection with lowMemoryNotification between - // page navigations to keep v8 memory usage as low as possible. - // Calling immediately after a runMessageLoop ensure - self._session.browser.env.lowMemoryNotification(); + + // We force a garbage collection between page navigations to keep v8 + // memory usage as low as possible. + self._session.browser.env.memoryPressureNotification(.moderate); self._script_manager.shutdown = true; self._session.browser.http_client.abort(); @@ -317,6 +331,9 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._element_shadow_roots = .{}; self._node_owner_documents = .{}; self._element_assigned_slots = .{}; + + self._element_attr_listeners = .{}; + self._notified_network_idle = .init; self._notified_network_almost_idle = .init; @@ -379,14 +396,23 @@ const GetArenaOpts = struct { pub fn getArena(self: *Page, comptime opts: GetArenaOpts) !Allocator { const allocator = try self.arena_pool.acquire(); if (comptime IS_DEBUG) { - try self._arena_pool_leak_track.put(self.arena, @intFromPtr(allocator.ptr), opts.debug); + const gop = try self._arena_pool_leak_track.getOrPut(self.arena, @intFromPtr(allocator.ptr)); + if (gop.found_existing) { + std.debug.assert(gop.value_ptr.count == 0); + } + gop.value_ptr.* = .{ .owner = opts.debug, .count = 1 }; } return allocator; } pub fn releaseArena(self: *Page, allocator: Allocator) void { if (comptime IS_DEBUG) { - _ = self._arena_pool_leak_track.remove(@intFromPtr(allocator.ptr)); + const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?; + if (found.count != 1) { + log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count }); + return; + } + found.count = 0; } return self.arena_pool.release(allocator); } @@ -746,7 +772,10 @@ fn pageDoneCallback(ctx: *anyopaque) !void { switch (self._parse_state) { .html => |buf| { - var parser = Parser.init(self.arena, self.document.asNode(), self); + const parse_arena = try self.getArena(.{ .debug = "Page.parse" }); + defer self.releaseArena(parse_arena); + + var parser = Parser.init(parse_arena, self.document.asNode(), self); parser.parse(buf.items); self._script_manager.staticScriptsDone(); if (self._script_manager.isDone()) { @@ -758,7 +787,11 @@ fn pageDoneCallback(ctx: *anyopaque) !void { }, .text => |*buf| { try buf.appendSlice(self.arena, ""); - var parser = Parser.init(self.arena, self.document.asNode(), self); + + const parse_arena = try self.getArena(.{ .debug = "Page.parse" }); + defer self.releaseArena(parse_arena); + + var parser = Parser.init(parse_arena, self.document.asNode(), self); parser.parse(buf.items); self.documentIsComplete(); }, @@ -1135,6 +1168,35 @@ pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Elemen return null; } +/// Sets an inline event listener (`onload`, `onclick`, `onwheel` etc.); +/// overrides the listener if there's already one. +pub fn setAttrListener( + self: *Page, + element: *Element, + listener_type: Element.KnownListener, + listener_callback: JS.Function.Global, +) !void { + if (comptime IS_DEBUG) { + log.debug(.event, "Page.setAttrListener", .{ + .element = element, + .listener_type = listener_type, + }); + } + + const key = element.calcAttrListenerKey(listener_type); + const gop = try self._element_attr_listeners.getOrPut(self.arena, key); + gop.value_ptr.* = listener_callback; +} + +/// Returns the inline event listener by an element and listener type. +pub fn getAttrListener( + self: *const Page, + element: *Element, + listener_type: Element.KnownListener, +) ?JS.Function.Global { + return self._element_attr_listeners.get(element.calcAttrListenerKey(listener_type)); +} + pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void { return self._performance_observers.append(self.arena, observer); } @@ -2174,6 +2236,236 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi } var attributes = try element.createAttributeList(self); while (list.next()) |attr| { + // Event handlers can be provided like attributes; here we check if there's such. + const name = attr.name.local; + lp.assert(name.len != 0, "populateElementAttributes: 0-length attr name", .{ .attr = attr }); + // Idea here is to make this check as cheap as possible. + const has_on_prefix = @as(u16, @bitCast([2]u8{ name.ptr[0], name.ptr[1 % name.len] })) == asUint("on"); + // We may have found an event handler. + if (has_on_prefix) { + // Must be usable as function. + const func = try self.js.stringToPersistedFunction(attr.value.slice()); + + // Longest known listener kind is 32 bytes long. + const remaining: u6 = @truncate(name.len -| 2); + const unsafe = name.ptr + 2; + const Vec16x8 = @Vector(16, u8); + const Vec32x8 = @Vector(32, u8); + + switch (remaining) { + 3 => if (@as(u24, @bitCast(unsafe[0..3].*)) == asUint("cut")) { + try self.setAttrListener(element, .cut, func); + }, + 4 => switch (@as(u32, @bitCast(unsafe[0..4].*))) { + asUint("blur") => try self.setAttrListener(element, .blur, func), + asUint("copy") => try self.setAttrListener(element, .copy, func), + asUint("drag") => try self.setAttrListener(element, .drag, func), + asUint("drop") => try self.setAttrListener(element, .drop, func), + asUint("load") => try self.setAttrListener(element, .load, func), + asUint("play") => try self.setAttrListener(element, .play, func), + else => {}, + }, + 5 => switch (@as(u40, @bitCast(unsafe[0..5].*))) { + asUint("abort") => try self.setAttrListener(element, .abort, func), + asUint("click") => try self.setAttrListener(element, .click, func), + asUint("close") => try self.setAttrListener(element, .close, func), + asUint("ended") => try self.setAttrListener(element, .ended, func), + asUint("error") => try self.setAttrListener(element, .@"error", func), + asUint("focus") => try self.setAttrListener(element, .focus, func), + asUint("input") => try self.setAttrListener(element, .input, func), + asUint("keyup") => try self.setAttrListener(element, .keyup, func), + asUint("paste") => try self.setAttrListener(element, .paste, func), + asUint("pause") => try self.setAttrListener(element, .pause, func), + asUint("reset") => try self.setAttrListener(element, .reset, func), + asUint("wheel") => try self.setAttrListener(element, .wheel, func), + else => {}, + }, + 6 => switch (@as(u48, @bitCast(unsafe[0..6].*))) { + asUint("cancel") => try self.setAttrListener(element, .cancel, func), + asUint("change") => try self.setAttrListener(element, .change, func), + asUint("resize") => try self.setAttrListener(element, .resize, func), + asUint("scroll") => try self.setAttrListener(element, .scroll, func), + asUint("seeked") => try self.setAttrListener(element, .seeked, func), + asUint("select") => try self.setAttrListener(element, .select, func), + asUint("submit") => try self.setAttrListener(element, .submit, func), + asUint("toggle") => try self.setAttrListener(element, .toggle, func), + else => {}, + }, + 7 => switch (@as(u56, @bitCast(unsafe[0..7].*))) { + asUint("canplay") => try self.setAttrListener(element, .canplay, func), + asUint("command") => try self.setAttrListener(element, .command, func), + asUint("dragend") => try self.setAttrListener(element, .dragend, func), + asUint("emptied") => try self.setAttrListener(element, .emptied, func), + asUint("invalid") => try self.setAttrListener(element, .invalid, func), + asUint("keydown") => try self.setAttrListener(element, .keydown, func), + asUint("mouseup") => try self.setAttrListener(element, .mouseup, func), + asUint("playing") => try self.setAttrListener(element, .playing, func), + asUint("seeking") => try self.setAttrListener(element, .seeking, func), + asUint("stalled") => try self.setAttrListener(element, .stalled, func), + asUint("suspend") => try self.setAttrListener(element, .@"suspend", func), + asUint("waiting") => try self.setAttrListener(element, .waiting, func), + else => {}, + }, + 8 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("auxclick") => try self.setAttrListener(element, .auxclick, func), + asUint("dblclick") => try self.setAttrListener(element, .dblclick, func), + asUint("dragexit") => try self.setAttrListener(element, .dragexit, func), + asUint("dragover") => try self.setAttrListener(element, .dragover, func), + asUint("formdata") => try self.setAttrListener(element, .formdata, func), + asUint("keypress") => try self.setAttrListener(element, .keypress, func), + asUint("mouseout") => try self.setAttrListener(element, .mouseout, func), + asUint("progress") => try self.setAttrListener(element, .progress, func), + else => {}, + }, + // Won't fit to 64-bit integer; we do 2 checks. + 9 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("cuechang") => if (unsafe[8] == 'e') try self.setAttrListener(element, .cuechange, func), + asUint("dragente") => if (unsafe[8] == 'r') try self.setAttrListener(element, .dragenter, func), + asUint("dragleav") => if (unsafe[8] == 'e') try self.setAttrListener(element, .dragleave, func), + asUint("dragstar") => if (unsafe[8] == 't') try self.setAttrListener(element, .dragstart, func), + asUint("loadstar") => if (unsafe[8] == 't') try self.setAttrListener(element, .loadstart, func), + asUint("mousedow") => if (unsafe[8] == 'n') try self.setAttrListener(element, .mousedown, func), + asUint("mousemov") => if (unsafe[8] == 'e') try self.setAttrListener(element, .mousemove, func), + asUint("mouseove") => if (unsafe[8] == 'r') try self.setAttrListener(element, .mouseover, func), + asUint("pointeru") => if (unsafe[8] == 'p') try self.setAttrListener(element, .pointerup, func), + asUint("scrollen") => if (unsafe[8] == 'd') try self.setAttrListener(element, .scrollend, func), + else => {}, + }, + 10 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("loadedda") => if (asUint("ta") == @as(u16, @bitCast(unsafe[8..10].*))) + try self.setAttrListener(element, .loadeddata, func), + asUint("pointero") => if (asUint("ut") == @as(u16, @bitCast(unsafe[8..10].*))) + try self.setAttrListener(element, .pointerout, func), + asUint("ratechan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*))) + try self.setAttrListener(element, .ratechange, func), + asUint("slotchan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*))) + try self.setAttrListener(element, .slotchange, func), + asUint("timeupda") => if (asUint("te") == @as(u16, @bitCast(unsafe[8..10].*))) + try self.setAttrListener(element, .timeupdate, func), + else => {}, + }, + 11 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("beforein") => if (asUint("put") == @as(u24, @bitCast(unsafe[8..11].*))) + try self.setAttrListener(element, .beforeinput, func), + asUint("beforema") => if (asUint("tch") == @as(u24, @bitCast(unsafe[8..11].*))) + try self.setAttrListener(element, .beforematch, func), + asUint("contextl") => if (asUint("ost") == @as(u24, @bitCast(unsafe[8..11].*))) + try self.setAttrListener(element, .contextlost, func), + asUint("contextm") => if (asUint("enu") == @as(u24, @bitCast(unsafe[8..11].*))) + try self.setAttrListener(element, .contextmenu, func), + asUint("pointerd") => if (asUint("own") == @as(u24, @bitCast(unsafe[8..11].*))) + try self.setAttrListener(element, .pointerdown, func), + asUint("pointerm") => if (asUint("ove") == @as(u24, @bitCast(unsafe[8..11].*))) + try self.setAttrListener(element, .pointermove, func), + asUint("pointero") => if (asUint("ver") == @as(u24, @bitCast(unsafe[8..11].*))) + try self.setAttrListener(element, .pointerover, func), + asUint("selectst") => if (asUint("art") == @as(u24, @bitCast(unsafe[8..11].*))) + try self.setAttrListener(element, .selectstart, func), + else => {}, + }, + 12 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("animatio") => if (asUint("nend") == @as(u32, @bitCast(unsafe[8..12].*))) + try self.setAttrListener(element, .animationend, func), + asUint("beforeto") => if (asUint("ggle") == @as(u32, @bitCast(unsafe[8..12].*))) + try self.setAttrListener(element, .beforetoggle, func), + asUint("pointere") => if (asUint("nter") == @as(u32, @bitCast(unsafe[8..12].*))) + try self.setAttrListener(element, .pointerenter, func), + asUint("pointerl") => if (asUint("eave") == @as(u32, @bitCast(unsafe[8..12].*))) + try self.setAttrListener(element, .pointerleave, func), + asUint("volumech") => if (asUint("ange") == @as(u32, @bitCast(unsafe[8..12].*))) + try self.setAttrListener(element, .volumechange, func), + else => {}, + }, + 13 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("pointerc") => if (asUint("ancel") == @as(u40, @bitCast(unsafe[8..13].*))) + try self.setAttrListener(element, .pointercancel, func), + asUint("transiti") => switch (@as(u40, @bitCast(unsafe[8..13].*))) { + asUint("onend") => try self.setAttrListener(element, .transitionend, func), + asUint("onrun") => try self.setAttrListener(element, .transitionrun, func), + else => {}, + }, + else => {}, + }, + 14 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("animatio") => if (asUint("nstart") == @as(u48, @bitCast(unsafe[8..14].*))) + try self.setAttrListener(element, .animationstart, func), + asUint("canplayt") => if (asUint("hrough") == @as(u48, @bitCast(unsafe[8..14].*))) + try self.setAttrListener(element, .canplaythrough, func), + asUint("duration") => if (asUint("change") == @as(u48, @bitCast(unsafe[8..14].*))) + try self.setAttrListener(element, .durationchange, func), + asUint("loadedme") => if (asUint("tadata") == @as(u48, @bitCast(unsafe[8..14].*))) + try self.setAttrListener(element, .loadedmetadata, func), + else => {}, + }, + 15 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("animatio") => if (asUint("ncancel") == @as(u56, @bitCast(unsafe[8..15].*))) + try self.setAttrListener(element, .animationcancel, func), + asUint("contextr") => if (asUint("estored") == @as(u56, @bitCast(unsafe[8..15].*))) + try self.setAttrListener(element, .contextrestored, func), + asUint("fullscre") => if (asUint("enerror") == @as(u56, @bitCast(unsafe[8..15].*))) + try self.setAttrListener(element, .fullscreenerror, func), + asUint("selectio") => if (asUint("nchange") == @as(u56, @bitCast(unsafe[8..15].*))) + try self.setAttrListener(element, .selectionchange, func), + asUint("transiti") => if (asUint("onstart") == @as(u56, @bitCast(unsafe[8..15].*))) + try self.setAttrListener(element, .transitionstart, func), + else => {}, + }, + // Can't switch on vector types. + 16 => { + const as_vector: Vec16x8 = unsafe[0..16].*; + + if (@reduce(.And, as_vector == @as(Vec16x8, "fullscreenchange".*))) { + try self.setAttrListener(element, .fullscreenchange, func); + } else if (@reduce(.And, as_vector == @as(Vec16x8, "pointerrawupdate".*))) { + try self.setAttrListener(element, .pointerrawupdate, func); + } else if (@reduce(.And, as_vector == @as(Vec16x8, "transitioncancel".*))) { + try self.setAttrListener(element, .transitioncancel, func); + } + }, + 17 => { + const as_vector: Vec16x8 = unsafe[0..16].*; + + const dirty = @reduce(.And, as_vector == @as(Vec16x8, "gotpointercaptur".*)) and + unsafe[16] == 'e'; + if (dirty) { + try self.setAttrListener(element, .gotpointercapture, func); + } + }, + 18 => { + const as_vector: Vec16x8 = unsafe[0..16].*; + + const is_animationiteration = @reduce(.And, as_vector == @as(Vec16x8, "animationiterati".*)) and + asUint("on") == @as(u16, @bitCast(unsafe[16..18].*)); + if (is_animationiteration) { + try self.setAttrListener(element, .animationiteration, func); + } else { + const is_lostpointercapture = @reduce(.And, as_vector == @as(Vec16x8, "lostpointercaptu".*)) and + asUint("re") == @as(u16, @bitCast(unsafe[16..18].*)); + if (is_lostpointercapture) { + try self.setAttrListener(element, .lostpointercapture, func); + } + } + }, + 23 => { + const as_vector: Vec16x8 = unsafe[0..16].*; + + const dirty = @reduce(.And, as_vector == @as(Vec16x8, "securitypolicyvi".*)) and + asUint("olation") == @as(u56, @bitCast(unsafe[16..23].*)); + if (dirty) { + try self.setAttrListener(element, .securitypolicyviolation, func); + } + }, + 32 => { + const as_vector: Vec32x8 = unsafe[0..32].*; + + if (@reduce(.And, as_vector == @as(Vec32x8, "contentvisibilityautostatechange".*))) { + try self.setAttrListener(element, .contentvisibilityautostatechange, func); + } + }, + else => {}, + } + } + try attributes.putNew(attr.name.local.slice(), attr.value.slice(), self); } } diff --git a/src/browser/URL.zig b/src/browser/URL.zig index c5177d95..356d2d55 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -95,7 +95,7 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime in_i += 2; continue; } - if (out[in_i + 1] == '.' and out[in_i + 2] == '/') { // always safe, because we added two whitespaces + if (out[in_i + 1] == '.' and out[in_i + 2] == '/') { // always safe, because we added two whitespaces // /../ if (out_i > path_marker) { // go back before the / diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 78fde66a..69f7cd20 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -24,6 +24,7 @@ const log = @import("../../log.zig"); const bridge = @import("bridge.zig"); const Context = @import("Context.zig"); +const Isolate = @import("Isolate.zig"); const Platform = @import("Platform.zig"); const Snapshot = @import("Snapshot.zig"); const Inspector = @import("Inspector.zig"); @@ -193,6 +194,8 @@ pub fn newExecutionWorld(self: *Env) !ExecutionWorld { // a Context, it's managed by the garbage collector. We use the // `lowMemoryNotification` call on the isolate to encourage v8 to free // any contexts which have been freed. +// This GC is very aggressive. Use memoryPressureNotification for less +// aggressive GC passes. pub fn lowMemoryNotification(self: *Env) void { var handle_scope: js.HandleScope = undefined; handle_scope.init(self.isolate); @@ -200,6 +203,21 @@ pub fn lowMemoryNotification(self: *Env) void { self.isolate.lowMemoryNotification(); } +// V8 doesn't immediately free memory associated with +// a Context, it's managed by the garbage collector. We use the +// `memoryPressureNotification` call on the isolate to encourage v8 to free +// any contexts which have been freed. +// The level indicates the aggressivity of the GC required: +// moderate speeds up incremental GC +// critical runs one full GC +// For a more aggressive GC, use lowMemoryNotification. +pub fn memoryPressureNotification(self: *Env, level: Isolate.MemoryPressureLevel) void { + var handle_scope: js.HandleScope = undefined; + handle_scope.init(self.isolate); + defer handle_scope.deinit(); + self.isolate.memoryPressureNotification(level); +} + pub fn dumpMemoryStats(self: *Env) void { const stats = self.isolate.getHeapStatistics(); std.debug.print( diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 8fecccfe..f30508a6 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -20,6 +20,8 @@ const std = @import("std"); const js = @import("js.zig"); const v8 = js.v8; +const log = @import("../../log.zig"); + const Allocator = std.mem.Allocator; const Function = @This(); @@ -69,25 +71,30 @@ pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Objec pub fn call(self: *const Function, comptime T: type, args: anytype) !T { var caught: js.TryCatch.Caught = undefined; - return self._tryCallWithThis(T, self.getThis(), args, &caught, false); + return self._tryCallWithThis(T, self.getThis(), args, &caught) catch |err| { + log.warn(.js, "call caught", .{ .err = err, .caught = caught }); + return err; + }; } pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T { var caught: js.TryCatch.Caught = undefined; - return self._tryCallWithThis(T, this, args, &caught, false); + return self._tryCallWithThis(T, this, args, &caught) catch |err| { + log.warn(.js, "callWithThis caught", .{ .err = err, .caught = caught }); + return err; + }; } pub fn tryCall(self: *const Function, comptime T: type, args: anytype, caught: *js.TryCatch.Caught) !T { - caught.* = .{}; - return self._tryCallWithThis(T, self.getThis(), args, caught, true); + return self._tryCallWithThis(T, self.getThis(), args, caught); } pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T { - caught.* = .{}; - return self._tryCallWithThis(T, this, args, caught, true); + return self._tryCallWithThis(T, this, args, caught); } -pub fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught, comptime need_caught: bool) !T { +pub fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, caught: *js.TryCatch.Caught) !T { + caught.* = .{}; const local = self.local; // When we're calling a function from within JavaScript itself, this isn't @@ -140,11 +147,7 @@ pub fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, defer try_catch.deinit(); const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse { - if (comptime need_caught) { - // relatively expensive, so if the caller knows caught won't be needed, - // we can leave it uninitialized. - caught.* = try_catch.caughtOrError(local.call_arena, error.JSExecCallback); - } + caught.* = try_catch.caughtOrError(local.call_arena, error.JSExecCallback); return error.JSExecCallback; }; diff --git a/src/browser/js/Inspector.zig b/src/browser/js/Inspector.zig index 1d35f3f5..13ad79ed 100644 --- a/src/browser/js/Inspector.zig +++ b/src/browser/js/Inspector.zig @@ -151,6 +151,18 @@ pub fn contextCreated( } } +pub fn contextDestroyed(self: *Inspector, local: *const js.Local) void { + v8.v8_inspector__Inspector__ContextDestroyed(self.handle, local.handle); +} + +pub fn resetContextGroup(self: *const Inspector) void { + var hs: v8.HandleScope = undefined; + v8.v8__HandleScope__CONSTRUCT(&hs, self.isolate); + defer v8.v8__HandleScope__DESTRUCT(&hs); + + v8.v8_inspector__Inspector__ResetContextGroup(self.handle, CONTEXT_GROUP_ID); +} + // Retrieves the RemoteObject for a given value. // The value is loaded through the ExecutionWorld's mapZigInstanceToJs function, // just like a method return value. Therefore, if we've mapped this diff --git a/src/browser/js/Isolate.zig b/src/browser/js/Isolate.zig index fdede915..74974cc0 100644 --- a/src/browser/js/Isolate.zig +++ b/src/browser/js/Isolate.zig @@ -57,6 +57,16 @@ pub fn lowMemoryNotification(self: Isolate) void { v8.v8__Isolate__LowMemoryNotification(self.handle); } +pub const MemoryPressureLevel = enum(u32) { + none = v8.kNone, + moderate = v8.kModerate, + critical = v8.kCritical, +}; + +pub fn memoryPressureNotification(self: Isolate, level: MemoryPressureLevel) void { + v8.v8__Isolate__MemoryPressureNotification(self.handle, @intFromEnum(level)); +} + pub fn notifyContextDisposed(self: Isolate) void { _ = v8.v8__Isolate__ContextDisposedNotification(self.handle); } diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index d2d952f4..2cb2acaa 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -24,6 +24,7 @@ const Page = @import("../Page.zig"); const Node = @import("../webapi/Node.zig"); const Element = @import("../webapi/Element.zig"); const Allocator = std.mem.Allocator; +const IS_DEBUG = @import("builtin").mode == .Debug; pub const ParsedNode = struct { node: *Node, @@ -373,6 +374,17 @@ fn _appendCallback(self: *Parser, parent: *Node, node_or_text: h5e.NodeOrText) ! switch (node_or_text.toUnion()) { .node => |cpn| { const child = getNode(cpn); + if (child._parent) |previous_parent| { + // html5ever says this can't happen, but we might be screwing up + // the node on our side. We shouldn't be, but we're seeing this + // in the wild, and I'm not sure why. In debug, let's crash so + // we can try to figure it out. In release, let's disconnect + // the child first. + if (comptime IS_DEBUG) { + unreachable; + } + self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() }); + } try self.page.appendNew(parent, .{ .node = child }); }, .text => |txt| try self.page.appendNew(parent, .{ .text = txt }), diff --git a/src/browser/tests/element/html/event_listeners.html b/src/browser/tests/element/html/event_listeners.html new file mode 100644 index 00000000..9e7f9ebd --- /dev/null +++ b/src/browser/tests/element/html/event_listeners.html @@ -0,0 +1,490 @@ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/DOMParser.zig b/src/browser/webapi/DOMParser.zig index c79f9956..30f8bf33 100644 --- a/src/browser/webapi/DOMParser.zig +++ b/src/browser/webapi/DOMParser.zig @@ -48,6 +48,9 @@ pub fn parseFromString( @"image/svg+xml", }, mime_type) orelse return error.NotSupported; + const arena = try page.getArena(.{ .debug = "DOMParser.parseFromString" }); + defer page.releaseArena(arena); + return switch (target_mime) { .@"text/html" => { // Create a new HTMLDocument @@ -61,7 +64,7 @@ pub fn parseFromString( } // Parse HTML into the document - var parser = Parser.init(page.arena, doc.asNode(), page); + var parser = Parser.init(arena, doc.asNode(), page); parser.parse(normalized); if (parser.err) |pe| { @@ -78,7 +81,7 @@ pub fn parseFromString( // Parse XML into XMLDocument. const doc_node = doc.asNode(); - var parser = Parser.init(page.arena, doc_node, page); + var parser = Parser.init(arena, doc_node, page); parser.parseXML(html); if (parser.err) |pe| { diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index e5c5f623..17b2639b 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -648,7 +648,10 @@ pub fn write(self: *Document, text: []const []const u8, page: *Page) !void { page._parse_mode = .document_write; defer page._parse_mode = previous_parse_mode; - var parser = Parser.init(page.call_arena, fragment_node, page); + const arena = try page.getArena(.{ .debug = "Document.write" }); + defer page.releaseArena(arena); + + var parser = Parser.init(arena, fragment_node, page); parser.parseFragment(html); // Extract children from wrapper HTML element (html5ever wraps fragments) @@ -661,7 +664,7 @@ pub fn write(self: *Document, text: []const []const u8, page: *Page) !void { var it = if (first.is(Element.Html.Html) == null) fragment_node.childrenIterator() else first.childrenIterator(); while (it.next()) |child| { - try children_to_insert.append(page.call_arena, child); + try children_to_insert.append(arena, child); } if (children_to_insert.items.len == 0) { diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index f983b514..619d95cd 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -49,6 +49,129 @@ pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTok pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot); pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot); +/// Better to discriminate it since not directly a pointer int. +/// +/// See `calcAttrListenerKey` to obtain one. +const AttrListenerKey = u64; +/// Use `getAttrListenerKey` to create a key. +pub const AttrListenerLookup = std.AutoHashMapUnmanaged(AttrListenerKey, js.Function.Global); + +/// Enum of known event listeners; increasing the size of it (u7) +/// can cause `AttrListenerKey` to behave incorrectly. +pub const KnownListener = enum(u7) { + abort, + animationcancel, + animationend, + animationiteration, + animationstart, + auxclick, + beforeinput, + beforematch, + beforetoggle, + blur, + cancel, + canplay, + canplaythrough, + change, + click, + close, + command, + contentvisibilityautostatechange, + contextlost, + contextmenu, + contextrestored, + copy, + cuechange, + cut, + dblclick, + drag, + dragend, + dragenter, + dragexit, + dragleave, + dragover, + dragstart, + drop, + durationchange, + emptied, + ended, + @"error", + focus, + formdata, + fullscreenchange, + fullscreenerror, + gotpointercapture, + input, + invalid, + keydown, + keypress, + keyup, + load, + loadeddata, + loadedmetadata, + loadstart, + lostpointercapture, + mousedown, + mousemove, + mouseout, + mouseover, + mouseup, + paste, + pause, + play, + playing, + pointercancel, + pointerdown, + pointerenter, + pointerleave, + pointermove, + pointerout, + pointerover, + pointerrawupdate, + pointerup, + progress, + ratechange, + reset, + resize, + scroll, + scrollend, + securitypolicyviolation, + seeked, + seeking, + select, + selectionchange, + selectstart, + slotchange, + stalled, + submit, + @"suspend", + timeupdate, + toggle, + transitioncancel, + transitionend, + transitionrun, + transitionstart, + volumechange, + waiting, + wheel, +}; + +/// Calculates a lookup key (`AttrListenerKey`) to use with `AttrListenerLookup` for an element. +/// NEVER use generated key to retrieve a pointer back. Portability is not guaranteed. +pub fn calcAttrListenerKey(self: *Element, event_type: KnownListener) AttrListenerKey { + // We can use `Element` for the key too; `EventTarget` is strict about + // its size and alignment, though. + const target = self.asEventTarget(); + // Check if we have 3 bits available from alignment of 8. + lp.assert(@alignOf(@TypeOf(target)) == 8, "createLookupKey: incorrect alignment", .{ + .event_target_alignment = @alignOf(@TypeOf(target)), + }); + + const ptr = @intFromPtr(target) >> 3; + lp.assert(ptr < (1 << 57), "createLookupKey: pointer overflow", .{ .ptr = ptr }); + return ptr | (@as(u64, @intFromEnum(event_type)) << 57); +} + pub const Namespace = enum(u8) { html, svg, diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 9eb41edd..639231b5 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -281,8 +281,11 @@ pub fn insertAdjacentHTML( }); const doc_node = doc.asNode(); + const arena = try page.getArena(.{ .debug = "HTML.insertAdjacentHTML" }); + defer page.releaseArena(arena); + const Parser = @import("../../parser/Parser.zig"); - var parser = Parser.init(page.call_arena, doc_node, page); + var parser = Parser.init(arena, doc_node, page); parser.parse(html); // Check if there's parsing error. @@ -330,6 +333,766 @@ pub fn click(self: *HtmlElement, page: *Page) !void { try page._event_manager.dispatch(self.asEventTarget(), event.asEvent()); } +pub fn setOnAbort(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .abort, callback); +} + +pub fn getOnAbort(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .abort); +} + +pub fn setOnAnimationCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .animationcancel, callback); +} + +pub fn getOnAnimationCancel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .animationcancel); +} + +pub fn setOnAnimationEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .animationend, callback); +} + +pub fn getOnAnimationEnd(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .animationend); +} + +pub fn setOnAnimationIteration(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .animationiteration, callback); +} + +pub fn getOnAnimationIteration(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .animationiteration); +} + +pub fn setOnAnimationStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .animationstart, callback); +} + +pub fn getOnAnimationStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .animationstart); +} + +pub fn setOnAuxClick(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .auxclick, callback); +} + +pub fn getOnAuxClick(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .auxclick); +} + +pub fn setOnBeforeInput(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .beforeinput, callback); +} + +pub fn getOnBeforeInput(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .beforeinput); +} + +pub fn setOnBeforeMatch(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .beforematch, callback); +} + +pub fn getOnBeforeMatch(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .beforematch); +} + +pub fn setOnBeforeToggle(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .beforetoggle, callback); +} + +pub fn getOnBeforeToggle(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .beforetoggle); +} + +pub fn setOnBlur(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .blur, callback); +} + +pub fn getOnBlur(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .blur); +} + +pub fn setOnCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .cancel, callback); +} + +pub fn getOnCancel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .cancel); +} + +pub fn setOnCanPlay(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .canplay, callback); +} + +pub fn getOnCanPlay(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .canplay); +} + +pub fn setOnCanPlayThrough(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .canplaythrough, callback); +} + +pub fn getOnCanPlayThrough(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .canplaythrough); +} + +pub fn setOnChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .change, callback); +} + +pub fn getOnChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .change); +} + +pub fn setOnClick(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .click, callback); +} + +pub fn getOnClick(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .click); +} + +pub fn setOnClose(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .close, callback); +} + +pub fn getOnClose(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .close); +} + +pub fn setOnCommand(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .command, callback); +} + +pub fn getOnCommand(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .command); +} + +pub fn setOnContentVisibilityAutoStateChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .contentvisibilityautostatechange, callback); +} + +pub fn getOnContentVisibilityAutoStateChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .contentvisibilityautostatechange); +} + +pub fn setOnContextLost(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .contextlost, callback); +} + +pub fn getOnContextLost(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .contextlost); +} + +pub fn setOnContextMenu(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .contextmenu, callback); +} + +pub fn getOnContextMenu(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .contextmenu); +} + +pub fn setOnContextRestored(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .contextrestored, callback); +} + +pub fn getOnContextRestored(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .contextrestored); +} + +pub fn setOnCopy(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .copy, callback); +} + +pub fn getOnCopy(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .copy); +} + +pub fn setOnCueChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .cuechange, callback); +} + +pub fn getOnCueChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .cuechange); +} + +pub fn setOnCut(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .cut, callback); +} + +pub fn getOnCut(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .cut); +} + +pub fn setOnDblClick(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dblclick, callback); +} + +pub fn getOnDblClick(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dblclick); +} + +pub fn setOnDrag(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .drag, callback); +} + +pub fn getOnDrag(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .drag); +} + +pub fn setOnDragEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragend, callback); +} + +pub fn getOnDragEnd(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragend); +} + +pub fn setOnDragEnter(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragenter, callback); +} + +pub fn getOnDragEnter(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragenter); +} + +pub fn setOnDragExit(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragexit, callback); +} + +pub fn getOnDragExit(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragexit); +} + +pub fn setOnDragLeave(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragleave, callback); +} + +pub fn getOnDragLeave(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragleave); +} + +pub fn setOnDragOver(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragover, callback); +} + +pub fn getOnDragOver(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragover); +} + +pub fn setOnDragStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragstart, callback); +} + +pub fn getOnDragStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragstart); +} + +pub fn setOnDrop(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .drop, callback); +} + +pub fn getOnDrop(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .drop); +} + +pub fn setOnDurationChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .durationchange, callback); +} + +pub fn getOnDurationChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .durationchange); +} + +pub fn setOnEmptied(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .emptied, callback); +} + +pub fn getOnEmptied(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .emptied); +} + +pub fn setOnEnded(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .ended, callback); +} + +pub fn getOnEnded(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .ended); +} + +pub fn setOnError(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .@"error", callback); +} + +pub fn getOnError(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .@"error"); +} + +pub fn setOnFocus(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .focus, callback); +} + +pub fn getOnFocus(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .focus); +} + +pub fn setOnFormData(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .formdata, callback); +} + +pub fn getOnFormData(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .formdata); +} + +pub fn setOnFullscreenChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .fullscreenchange, callback); +} + +pub fn getOnFullscreenChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .fullscreenchange); +} + +pub fn setOnFullscreenError(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .fullscreenerror, callback); +} + +pub fn getOnFullscreenError(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .fullscreenerror); +} + +pub fn setOnGotPointerCapture(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .gotpointercapture, callback); +} + +pub fn getOnGotPointerCapture(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .gotpointercapture); +} + +pub fn setOnInput(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .input, callback); +} + +pub fn getOnInput(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .input); +} + +pub fn setOnInvalid(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .invalid, callback); +} + +pub fn getOnInvalid(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .invalid); +} + +pub fn setOnKeyDown(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .keydown, callback); +} + +pub fn getOnKeyDown(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .keydown); +} + +pub fn setOnKeyPress(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .keypress, callback); +} + +pub fn getOnKeyPress(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .keypress); +} + +pub fn setOnKeyUp(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .keyup, callback); +} + +pub fn getOnKeyUp(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .keyup); +} + +pub fn setOnLoad(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .load, callback); +} + +pub fn getOnLoad(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .load); +} + +pub fn setOnLoadedData(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .loadeddata, callback); +} + +pub fn getOnLoadedData(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .loadeddata); +} + +pub fn setOnLoadedMetadata(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .loadedmetadata, callback); +} + +pub fn getOnLoadedMetadata(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .loadedmetadata); +} + +pub fn setOnLoadStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .loadstart, callback); +} + +pub fn getOnLoadStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .loadstart); +} + +pub fn setOnLostPointerCapture(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .lostpointercapture, callback); +} + +pub fn getOnLostPointerCapture(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .lostpointercapture); +} + +pub fn setOnMouseDown(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mousedown, callback); +} + +pub fn getOnMouseDown(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mousedown); +} + +pub fn setOnMouseMove(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mousemove, callback); +} + +pub fn getOnMouseMove(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mousemove); +} + +pub fn setOnMouseOut(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mouseout, callback); +} + +pub fn getOnMouseOut(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mouseout); +} + +pub fn setOnMouseOver(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mouseover, callback); +} + +pub fn getOnMouseOver(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mouseover); +} + +pub fn setOnMouseUp(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mouseup, callback); +} + +pub fn getOnMouseUp(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mouseup); +} + +pub fn setOnPaste(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .paste, callback); +} + +pub fn getOnPaste(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .paste); +} + +pub fn setOnPause(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pause, callback); +} + +pub fn getOnPause(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pause); +} + +pub fn setOnPlay(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .play, callback); +} + +pub fn getOnPlay(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .play); +} + +pub fn setOnPlaying(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .playing, callback); +} + +pub fn getOnPlaying(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .playing); +} + +pub fn setOnPointerCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointercancel, callback); +} + +pub fn getOnPointerCancel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointercancel); +} + +pub fn setOnPointerDown(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerdown, callback); +} + +pub fn getOnPointerDown(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerdown); +} + +pub fn setOnPointerEnter(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerenter, callback); +} + +pub fn getOnPointerEnter(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerenter); +} + +pub fn setOnPointerLeave(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerleave, callback); +} + +pub fn getOnPointerLeave(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerleave); +} + +pub fn setOnPointerMove(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointermove, callback); +} + +pub fn getOnPointerMove(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointermove); +} + +pub fn setOnPointerOut(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerout, callback); +} + +pub fn getOnPointerOut(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerout); +} + +pub fn setOnPointerOver(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerover, callback); +} + +pub fn getOnPointerOver(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerover); +} + +pub fn setOnPointerRawUpdate(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerrawupdate, callback); +} + +pub fn getOnPointerRawUpdate(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerrawupdate); +} + +pub fn setOnPointerUp(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerup, callback); +} + +pub fn getOnPointerUp(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerup); +} + +pub fn setOnProgress(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .progress, callback); +} + +pub fn getOnProgress(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .progress); +} + +pub fn setOnRateChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .ratechange, callback); +} + +pub fn getOnRateChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .ratechange); +} + +pub fn setOnReset(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .reset, callback); +} + +pub fn getOnReset(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .reset); +} + +pub fn setOnResize(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .resize, callback); +} + +pub fn getOnResize(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .resize); +} + +pub fn setOnScroll(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .scroll, callback); +} + +pub fn getOnScroll(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .scroll); +} + +pub fn setOnScrollEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .scrollend, callback); +} + +pub fn getOnScrollEnd(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .scrollend); +} + +pub fn setOnSecurityPolicyViolation(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .securitypolicyviolation, callback); +} + +pub fn getOnSecurityPolicyViolation(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .securitypolicyviolation); +} + +pub fn setOnSeeked(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .seeked, callback); +} + +pub fn getOnSeeked(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .seeked); +} + +pub fn setOnSeeking(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .seeking, callback); +} + +pub fn getOnSeeking(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .seeking); +} + +pub fn setOnSelect(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .select, callback); +} + +pub fn getOnSelect(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .select); +} + +pub fn setOnSelectionChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .selectionchange, callback); +} + +pub fn getOnSelectionChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .selectionchange); +} + +pub fn setOnSelectStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .selectstart, callback); +} + +pub fn getOnSelectStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .selectstart); +} + +pub fn setOnSlotChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .slotchange, callback); +} + +pub fn getOnSlotChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .slotchange); +} + +pub fn setOnStalled(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .stalled, callback); +} + +pub fn getOnStalled(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .stalled); +} + +pub fn setOnSubmit(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .submit, callback); +} + +pub fn getOnSubmit(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .submit); +} + +pub fn setOnSuspend(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .@"suspend", callback); +} + +pub fn getOnSuspend(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .@"suspend"); +} + +pub fn setOnTimeUpdate(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .timeupdate, callback); +} + +pub fn getOnTimeUpdate(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .timeupdate); +} + +pub fn setOnToggle(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .toggle, callback); +} + +pub fn getOnToggle(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .toggle); +} + +pub fn setOnTransitionCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .transitioncancel, callback); +} + +pub fn getOnTransitionCancel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .transitioncancel); +} + +pub fn setOnTransitionEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .transitionend, callback); +} + +pub fn getOnTransitionEnd(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .transitionend); +} + +pub fn setOnTransitionRun(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .transitionrun, callback); +} + +pub fn getOnTransitionRun(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .transitionrun); +} + +pub fn setOnTransitionStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .transitionstart, callback); +} + +pub fn getOnTransitionStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .transitionstart); +} + +pub fn setOnVolumeChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .volumechange, callback); +} + +pub fn getOnVolumeChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .volumechange); +} + +pub fn setOnWaiting(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .waiting, callback); +} + +pub fn getOnWaiting(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .waiting); +} + +pub fn setOnWheel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .wheel, callback); +} + +pub fn getOnWheel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .wheel); +} + pub const JsApi = struct { pub const bridge = js.Bridge(HtmlElement); @@ -349,6 +1112,102 @@ pub const JsApi = struct { } pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true }); pub const click = bridge.function(HtmlElement.click, .{}); + + pub const onabort = bridge.accessor(HtmlElement.getOnAbort, HtmlElement.setOnAbort, .{}); + pub const onanimationcancel = bridge.accessor(HtmlElement.getOnAnimationCancel, HtmlElement.setOnAnimationCancel, .{}); + pub const onanimationend = bridge.accessor(HtmlElement.getOnAnimationEnd, HtmlElement.setOnAnimationEnd, .{}); + pub const onanimationiteration = bridge.accessor(HtmlElement.getOnAnimationIteration, HtmlElement.setOnAnimationIteration, .{}); + pub const onanimationstart = bridge.accessor(HtmlElement.getOnAnimationStart, HtmlElement.setOnAnimationStart, .{}); + pub const onauxclick = bridge.accessor(HtmlElement.getOnAuxClick, HtmlElement.setOnAuxClick, .{}); + pub const onbeforeinput = bridge.accessor(HtmlElement.getOnBeforeInput, HtmlElement.setOnBeforeInput, .{}); + pub const onbeforematch = bridge.accessor(HtmlElement.getOnBeforeMatch, HtmlElement.setOnBeforeMatch, .{}); + pub const onbeforetoggle = bridge.accessor(HtmlElement.getOnBeforeToggle, HtmlElement.setOnBeforeToggle, .{}); + pub const onblur = bridge.accessor(HtmlElement.getOnBlur, HtmlElement.setOnBlur, .{}); + pub const oncancel = bridge.accessor(HtmlElement.getOnCancel, HtmlElement.setOnCancel, .{}); + pub const oncanplay = bridge.accessor(HtmlElement.getOnCanPlay, HtmlElement.setOnCanPlay, .{}); + pub const oncanplaythrough = bridge.accessor(HtmlElement.getOnCanPlayThrough, HtmlElement.setOnCanPlayThrough, .{}); + pub const onchange = bridge.accessor(HtmlElement.getOnChange, HtmlElement.setOnChange, .{}); + pub const onclick = bridge.accessor(HtmlElement.getOnClick, HtmlElement.setOnClick, .{}); + pub const onclose = bridge.accessor(HtmlElement.getOnClose, HtmlElement.setOnClose, .{}); + pub const oncommand = bridge.accessor(HtmlElement.getOnCommand, HtmlElement.setOnCommand, .{}); + pub const oncontentvisibilityautostatechange = bridge.accessor(HtmlElement.getOnContentVisibilityAutoStateChange, HtmlElement.setOnContentVisibilityAutoStateChange, .{}); + pub const oncontextlost = bridge.accessor(HtmlElement.getOnContextLost, HtmlElement.setOnContextLost, .{}); + pub const oncontextmenu = bridge.accessor(HtmlElement.getOnContextMenu, HtmlElement.setOnContextMenu, .{}); + pub const oncontextrestored = bridge.accessor(HtmlElement.getOnContextRestored, HtmlElement.setOnContextRestored, .{}); + pub const oncopy = bridge.accessor(HtmlElement.getOnCopy, HtmlElement.setOnCopy, .{}); + pub const oncuechange = bridge.accessor(HtmlElement.getOnCueChange, HtmlElement.setOnCueChange, .{}); + pub const oncut = bridge.accessor(HtmlElement.getOnCut, HtmlElement.setOnCut, .{}); + pub const ondblclick = bridge.accessor(HtmlElement.getOnDblClick, HtmlElement.setOnDblClick, .{}); + pub const ondrag = bridge.accessor(HtmlElement.getOnDrag, HtmlElement.setOnDrag, .{}); + pub const ondragend = bridge.accessor(HtmlElement.getOnDragEnd, HtmlElement.setOnDragEnd, .{}); + pub const ondragenter = bridge.accessor(HtmlElement.getOnDragEnter, HtmlElement.setOnDragEnter, .{}); + pub const ondragexit = bridge.accessor(HtmlElement.getOnDragExit, HtmlElement.setOnDragExit, .{}); + pub const ondragleave = bridge.accessor(HtmlElement.getOnDragLeave, HtmlElement.setOnDragLeave, .{}); + pub const ondragover = bridge.accessor(HtmlElement.getOnDragOver, HtmlElement.setOnDragOver, .{}); + pub const ondragstart = bridge.accessor(HtmlElement.getOnDragStart, HtmlElement.setOnDragStart, .{}); + pub const ondrop = bridge.accessor(HtmlElement.getOnDrop, HtmlElement.setOnDrop, .{}); + pub const ondurationchange = bridge.accessor(HtmlElement.getOnDurationChange, HtmlElement.setOnDurationChange, .{}); + pub const onemptied = bridge.accessor(HtmlElement.getOnEmptied, HtmlElement.setOnEmptied, .{}); + pub const onended = bridge.accessor(HtmlElement.getOnEnded, HtmlElement.setOnEnded, .{}); + pub const onerror = bridge.accessor(HtmlElement.getOnError, HtmlElement.setOnError, .{}); + pub const onfocus = bridge.accessor(HtmlElement.getOnFocus, HtmlElement.setOnFocus, .{}); + pub const onformdata = bridge.accessor(HtmlElement.getOnFormData, HtmlElement.setOnFormData, .{}); + pub const onfullscreenchange = bridge.accessor(HtmlElement.getOnFullscreenChange, HtmlElement.setOnFullscreenChange, .{}); + pub const onfullscreenerror = bridge.accessor(HtmlElement.getOnFullscreenError, HtmlElement.setOnFullscreenError, .{}); + pub const ongotpointercapture = bridge.accessor(HtmlElement.getOnGotPointerCapture, HtmlElement.setOnGotPointerCapture, .{}); + pub const oninput = bridge.accessor(HtmlElement.getOnInput, HtmlElement.setOnInput, .{}); + pub const oninvalid = bridge.accessor(HtmlElement.getOnInvalid, HtmlElement.setOnInvalid, .{}); + pub const onkeydown = bridge.accessor(HtmlElement.getOnKeyDown, HtmlElement.setOnKeyDown, .{}); + pub const onkeypress = bridge.accessor(HtmlElement.getOnKeyPress, HtmlElement.setOnKeyPress, .{}); + pub const onkeyup = bridge.accessor(HtmlElement.getOnKeyUp, HtmlElement.setOnKeyUp, .{}); + pub const onload = bridge.accessor(HtmlElement.getOnLoad, HtmlElement.setOnLoad, .{}); + pub const onloadeddata = bridge.accessor(HtmlElement.getOnLoadedData, HtmlElement.setOnLoadedData, .{}); + pub const onloadedmetadata = bridge.accessor(HtmlElement.getOnLoadedMetadata, HtmlElement.setOnLoadedMetadata, .{}); + pub const onloadstart = bridge.accessor(HtmlElement.getOnLoadStart, HtmlElement.setOnLoadStart, .{}); + pub const onlostpointercapture = bridge.accessor(HtmlElement.getOnLostPointerCapture, HtmlElement.setOnLostPointerCapture, .{}); + pub const onmousedown = bridge.accessor(HtmlElement.getOnMouseDown, HtmlElement.setOnMouseDown, .{}); + pub const onmousemove = bridge.accessor(HtmlElement.getOnMouseMove, HtmlElement.setOnMouseMove, .{}); + pub const onmouseout = bridge.accessor(HtmlElement.getOnMouseOut, HtmlElement.setOnMouseOut, .{}); + pub const onmouseover = bridge.accessor(HtmlElement.getOnMouseOver, HtmlElement.setOnMouseOver, .{}); + pub const onmouseup = bridge.accessor(HtmlElement.getOnMouseUp, HtmlElement.setOnMouseUp, .{}); + pub const onpaste = bridge.accessor(HtmlElement.getOnPaste, HtmlElement.setOnPaste, .{}); + pub const onpause = bridge.accessor(HtmlElement.getOnPause, HtmlElement.setOnPause, .{}); + pub const onplay = bridge.accessor(HtmlElement.getOnPlay, HtmlElement.setOnPlay, .{}); + pub const onplaying = bridge.accessor(HtmlElement.getOnPlaying, HtmlElement.setOnPlaying, .{}); + pub const onpointercancel = bridge.accessor(HtmlElement.getOnPointerCancel, HtmlElement.setOnPointerCancel, .{}); + pub const onpointerdown = bridge.accessor(HtmlElement.getOnPointerDown, HtmlElement.setOnPointerDown, .{}); + pub const onpointerenter = bridge.accessor(HtmlElement.getOnPointerEnter, HtmlElement.setOnPointerEnter, .{}); + pub const onpointerleave = bridge.accessor(HtmlElement.getOnPointerLeave, HtmlElement.setOnPointerLeave, .{}); + pub const onpointermove = bridge.accessor(HtmlElement.getOnPointerMove, HtmlElement.setOnPointerMove, .{}); + pub const onpointerout = bridge.accessor(HtmlElement.getOnPointerOut, HtmlElement.setOnPointerOut, .{}); + pub const onpointerover = bridge.accessor(HtmlElement.getOnPointerOver, HtmlElement.setOnPointerOver, .{}); + pub const onpointerrawupdate = bridge.accessor(HtmlElement.getOnPointerRawUpdate, HtmlElement.setOnPointerRawUpdate, .{}); + pub const onpointerup = bridge.accessor(HtmlElement.getOnPointerUp, HtmlElement.setOnPointerUp, .{}); + pub const onprogress = bridge.accessor(HtmlElement.getOnProgress, HtmlElement.setOnProgress, .{}); + pub const onratechange = bridge.accessor(HtmlElement.getOnRateChange, HtmlElement.setOnRateChange, .{}); + pub const onreset = bridge.accessor(HtmlElement.getOnReset, HtmlElement.setOnReset, .{}); + pub const onresize = bridge.accessor(HtmlElement.getOnResize, HtmlElement.setOnResize, .{}); + pub const onscroll = bridge.accessor(HtmlElement.getOnScroll, HtmlElement.setOnScroll, .{}); + pub const onscrollend = bridge.accessor(HtmlElement.getOnScrollEnd, HtmlElement.setOnScrollEnd, .{}); + pub const onsecuritypolicyviolation = bridge.accessor(HtmlElement.getOnSecurityPolicyViolation, HtmlElement.setOnSecurityPolicyViolation, .{}); + pub const onseeked = bridge.accessor(HtmlElement.getOnSeeked, HtmlElement.setOnSeeked, .{}); + pub const onseeking = bridge.accessor(HtmlElement.getOnSeeking, HtmlElement.setOnSeeking, .{}); + pub const onselect = bridge.accessor(HtmlElement.getOnSelect, HtmlElement.setOnSelect, .{}); + pub const onselectionchange = bridge.accessor(HtmlElement.getOnSelectionChange, HtmlElement.setOnSelectionChange, .{}); + pub const onselectstart = bridge.accessor(HtmlElement.getOnSelectStart, HtmlElement.setOnSelectStart, .{}); + pub const onslotchange = bridge.accessor(HtmlElement.getOnSlotChange, HtmlElement.setOnSlotChange, .{}); + pub const onstalled = bridge.accessor(HtmlElement.getOnStalled, HtmlElement.setOnStalled, .{}); + pub const onsubmit = bridge.accessor(HtmlElement.getOnSubmit, HtmlElement.setOnSubmit, .{}); + pub const onsuspend = bridge.accessor(HtmlElement.getOnSuspend, HtmlElement.setOnSuspend, .{}); + pub const ontimeupdate = bridge.accessor(HtmlElement.getOnTimeUpdate, HtmlElement.setOnTimeUpdate, .{}); + pub const ontoggle = bridge.accessor(HtmlElement.getOnToggle, HtmlElement.setOnToggle, .{}); + pub const ontransitioncancel = bridge.accessor(HtmlElement.getOnTransitionCancel, HtmlElement.setOnTransitionCancel, .{}); + pub const ontransitionend = bridge.accessor(HtmlElement.getOnTransitionEnd, HtmlElement.setOnTransitionEnd, .{}); + pub const ontransitionrun = bridge.accessor(HtmlElement.getOnTransitionRun, HtmlElement.setOnTransitionRun, .{}); + pub const ontransitionstart = bridge.accessor(HtmlElement.getOnTransitionStart, HtmlElement.setOnTransitionStart, .{}); + pub const onvolumechange = bridge.accessor(HtmlElement.getOnVolumeChange, HtmlElement.setOnVolumeChange, .{}); + pub const onwaiting = bridge.accessor(HtmlElement.getOnWaiting, HtmlElement.setOnWaiting, .{}); + pub const onwheel = bridge.accessor(HtmlElement.getOnWheel, HtmlElement.setOnWheel, .{}); }; pub const Build = struct { @@ -379,3 +1238,8 @@ pub const Build = struct { return false; } }; + +const testing = @import("../../../testing.zig"); +test "WebApi: HTML.event_listeners" { + try testing.htmlRunner("element/html/event_listeners.html", .{}); +} diff --git a/src/browser/webapi/encoding/TextDecoder.zig b/src/browser/webapi/encoding/TextDecoder.zig index 3148868b..ad4bf0f6 100644 --- a/src/browser/webapi/encoding/TextDecoder.zig +++ b/src/browser/webapi/encoding/TextDecoder.zig @@ -25,8 +25,9 @@ const Allocator = std.mem.Allocator; const TextDecoder = @This(); _fatal: bool, -_ignore_bom: bool, +_page: *Page, _arena: Allocator, +_ignore_bom: bool, _stream: std.ArrayListUnmanaged(u8), const Label = enum { @@ -45,13 +46,23 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder { _ = std.meta.stringToEnum(Label, label) orelse return error.RangeError; } + const arena = try page.getArena(.{ .debug = "TextDecoder" }); + errdefer page.releaseArena(arena); + const opts = opts_ orelse InitOpts{}; - return page._factory.create(TextDecoder{ - ._arena = page.arena, + const self = try arena.create(TextDecoder); + self.* = .{ + ._page = page, + ._arena = arena, ._stream = .empty, ._fatal = opts.fatal, ._ignore_bom = opts.ignoreBOM, - }); + }; + return self; +} + +pub fn deinit(self: *TextDecoder, _: bool) void { + self._page.releaseArena(self._arena); } pub fn getEncoding(_: *const TextDecoder) []const u8 { @@ -103,6 +114,8 @@ pub const JsApi = struct { pub const name = "TextDecoder"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const weak = true; + pub const finalizer = bridge.finalizer(TextDecoder.deinit); }; pub const constructor = bridge.constructor(TextDecoder.init, .{}); diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index d11dd8bd..2997a438 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -356,7 +356,7 @@ test "cdp Node: Registry register" { } { - const dom_node = (try doc.querySelector(.wrap ("p"), page)).?.asNode(); + const dom_node = (try doc.querySelector(.wrap("p"), page)).?.asNode(); const node = try registry.register(dom_node); const n1b = registry.lookup_by_id.get(2).?; const n1c = registry.lookup_by_node.get(node.dom).?; @@ -400,18 +400,18 @@ test "cdp Node: search list" { defer page._session.removePage(); var doc = page.window._document; - const s1 = try search_list.create((try doc.querySelectorAll(.wrap ("a"), page))._nodes); + const s1 = try search_list.create((try doc.querySelectorAll(.wrap("a"), page))._nodes); try testing.expectEqual("1", s1.name); try testing.expectEqualSlices(u32, &.{ 1, 2 }, s1.node_ids); try testing.expectEqual(2, registry.lookup_by_id.count()); try testing.expectEqual(2, registry.lookup_by_node.count()); - const s2 = try search_list.create((try doc.querySelectorAll(.wrap ("#a1"), page))._nodes); + const s2 = try search_list.create((try doc.querySelectorAll(.wrap("#a1"), page))._nodes); try testing.expectEqual("2", s2.name); try testing.expectEqualSlices(u32, &.{1}, s2.node_ids); - const s3 = try search_list.create((try doc.querySelectorAll(.wrap ("#a2"), page))._nodes); + const s3 = try search_list.create((try doc.querySelectorAll(.wrap("#a2"), page))._nodes); try testing.expectEqual("3", s3.name); try testing.expectEqualSlices(u32, &.{2}, s3.node_ids); diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index e5505cb6..b919e0c1 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -424,6 +424,11 @@ pub fn BrowserContext(comptime CDP_T: type) type { // in progress before deinit. self.cdp.browser.env.runMicrotasks(); + // resetContextGroup detach the inspector from all contexts. + // It append async tasks, so we make sure we run the message loop + // before deinit it. + self.inspector.resetContextGroup(); + self.session.browser.runMessageLoop(); self.inspector.deinit(); // abort all intercepted requests before closing the sesion/page