From de5b9364e64d0ab6054ee4d0208acd2275f2cf38 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 19 Jun 2026 10:11:17 +0800 Subject: [PATCH] cleanup: Break various frame behavior into their own little utility files This is purely mechanical and was driven almost entirely by Claude. The goal is simply to break Frame.zig into small / more cohesive components. So instead of having ~10 observer-specific methods on Frame, they now live in Frame.observers. In general, our code always takes *Frame as the last parameter (a consequence that this is the easiest / less ambiguous way to auto-inject a Frame into a webapi). But, for these, I kept it as the first parameter because it's still the "receiver" even though the dot syntax isn't applicable. --- src/browser/EventManager.zig | 4 +- src/browser/Frame.zig | 1694 +----------------- src/browser/frame/node_factory.zig | 1148 ++++++++++++ src/browser/frame/observers.zig | 258 +++ src/browser/frame/user_input.zig | 477 +++++ src/browser/js/Context.zig | 6 +- src/browser/parser/Parser.zig | 10 +- src/browser/webapi/CData.zig | 8 +- src/browser/webapi/DOMImplementation.zig | 12 +- src/browser/webapi/Document.zig | 20 +- src/browser/webapi/Element.zig | 4 +- src/browser/webapi/HTMLDocument.zig | 4 +- src/browser/webapi/IntersectionObserver.zig | 10 +- src/browser/webapi/MutationObserver.zig | 10 +- src/browser/webapi/Node.zig | 14 +- src/browser/webapi/Range.zig | 16 +- src/browser/webapi/cdata/Comment.zig | 2 +- src/browser/webapi/cdata/Text.zig | 4 +- src/browser/webapi/element/Html.zig | 4 +- src/browser/webapi/element/html/Audio.zig | 2 +- src/browser/webapi/element/html/Image.zig | 2 +- src/browser/webapi/element/html/TextArea.zig | 2 +- src/cdp/domains/input.zig | 15 +- 23 files changed, 1977 insertions(+), 1749 deletions(-) create mode 100644 src/browser/frame/node_factory.zig create mode 100644 src/browser/frame/observers.zig create mode 100644 src/browser/frame/user_input.zig diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 370d1ae84..863923d66 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -200,11 +200,11 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts if (event._prevent_default) { // can't return in a defer (╯°□°)╯︵ ┻━┻ } else if (event._type_string.eql(comptime .wrap("click"))) { - frame.handleClick(target) catch |err| { + Frame.user_input.handleClick(frame, target) catch |err| { log.warn(.event, "frame.click", .{ .err = err }); }; } else if (event._type_string.eql(comptime .wrap("keydown"))) { - frame.handleKeydown(target, event) catch |err| { + Frame.user_input.handleKeydown(frame, target, event) catch |err| { log.warn(.event, "frame.keydown", .{ .err = err }); }; } diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index b037daa93..43c9a4345 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -40,7 +40,6 @@ const FileList = @import("webapi/FileList.zig"); const Node = @import("webapi/Node.zig"); const Event = @import("webapi/Event.zig"); const EventTarget = @import("webapi/EventTarget.zig"); -const CData = @import("webapi/CData.zig"); const Text = @import("webapi/cdata/Text.zig"); const Element = @import("webapi/Element.zig"); const HtmlElement = @import("webapi/element/Html.zig"); @@ -62,9 +61,6 @@ const SubmitEvent = @import("webapi/event/SubmitEvent.zig"); const popover = @import("webapi/element/popover.zig"); const slotting = @import("webapi/element/slotting.zig"); const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind; -const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig"); -const MouseEvent = @import("webapi/event/MouseEvent.zig"); -const WheelEvent = @import("webapi/event/WheelEvent.zig"); const HttpClient = @import("HttpClient.zig"); @@ -73,6 +69,10 @@ const milliTimestamp = @import("../datetime.zig").milliTimestamp; const GlobalEventHandlersLookup = @import("webapi/global_event_handlers.zig").Lookup; +pub const observers = @import("frame/observers.zig"); +pub const user_input = @import("frame/user_input.zig"); +pub const node_factory = @import("frame/node_factory.zig"); + const log = lp.log; const String = lp.String; const IFrame = Element.Html.IFrame; @@ -181,15 +181,9 @@ _live_ranges: std.DoublyLinkedList = .{}, // channels in this frame's origin _broadcast_channels: std.DoublyLinkedList = .{}, -// List of active MutationObservers -_mutation_observers: std.DoublyLinkedList = .{}, -_mutation_delivery_scheduled: bool = false, -_mutation_delivery_depth: u32 = 0, - -// List of active IntersectionObservers -_intersection_observers: std.ArrayList(*IntersectionObserver) = .{}, -_intersection_check_scheduled: bool = false, -_intersection_delivery_scheduled: bool = false, +// MutationObserver / IntersectionObserver bookkeeping. See frame/observers.zig. +_mutation: observers.Mutation = .{}, +_intersection: observers.Intersection = .{}, // Slots that need slotchange events to be fired, in signal order. Delivered // by deliverMutations because there is specific timing with for these events @@ -456,18 +450,7 @@ pub fn deinit(self: *Frame) void { } } - { - var node: ?*std.DoublyLinkedList.Node = self._mutation_observers.first; - while (node) |n| { - node = n.next; // capture before we potentially delete observer - const observer: *MutationObserver = @fieldParentPtr("node", n); - observer.releaseRef(page); - } - } - - for (self._intersection_observers.items) |observer| { - observer.releaseRef(page); - } + observers.deinit(self, page); var document = self.window._document; document._selection.releaseRef(page); @@ -1641,11 +1624,11 @@ pub fn openPopup(self: *Frame, opts: OpenPopupOpts) !*Frame { pub fn domChanged(self: *Frame) void { self._page.dom_version += 1; - if (self._intersection_check_scheduled) { + if (self._intersection.check_scheduled) { return; } - self._intersection_check_scheduled = true; + self._intersection.check_scheduled = true; self.js.queueIntersectionChecks() catch |err| { log.err(.frame, "frame.schedIntersectChecks", .{ .err = err, .type = self._type, .url = self.url }); }; @@ -1740,42 +1723,11 @@ pub fn performance(self: *Frame) *Performance { return &self.window._performance; } -pub fn registerMutationObserver(self: *Frame, observer: *MutationObserver) !void { - observer.acquireRef(); - self._mutation_observers.append(&observer.node); -} - -pub fn unregisterMutationObserver(self: *Frame, observer: *MutationObserver) void { - observer.releaseRef(self._page); - self._mutation_observers.remove(&observer.node); -} - -pub fn registerIntersectionObserver(self: *Frame, observer: *IntersectionObserver) !void { - observer.acquireRef(); - try self._intersection_observers.append(self.arena, observer); -} - // Tracks a file input's FileList so its File refs are released at teardown. pub fn trackFileList(self: *Frame, file_list: *FileList) !void { try self._file_lists.append(self.arena, file_list); } -pub fn unregisterIntersectionObserver(self: *Frame, observer: *IntersectionObserver) void { - for (self._intersection_observers.items, 0..) |obs, i| { - if (obs == observer) { - observer.releaseRef(self._page); - _ = self._intersection_observers.swapRemove(i); - return; - } - } -} - -pub fn checkIntersections(self: *Frame) !void { - for (self._intersection_observers.items) |observer| { - try observer.checkIntersections(self); - } -} - pub const QueuedEvent = struct { kind: Kind, element: *Element.Html, @@ -1994,115 +1946,10 @@ fn dispatchQueuedEvents(self: *Frame) !void { to_process.clearRetainingCapacity(); } -pub fn scheduleMutationDelivery(self: *Frame) !void { - if (self._mutation_delivery_scheduled) { - return; - } - self._mutation_delivery_scheduled = true; - try self.js.queueMutationDelivery(); -} - -pub fn scheduleIntersectionDelivery(self: *Frame) !void { - if (self._intersection_delivery_scheduled) { - return; - } - self._intersection_delivery_scheduled = true; - try self.js.queueIntersectionDelivery(); -} - pub fn scheduleCustomElementBackupDrain(self: *Frame) !void { try self.js.queueCustomElementBackupDrain(); } -pub fn performScheduledIntersectionChecks(self: *Frame) void { - if (!self._intersection_check_scheduled) { - return; - } - self._intersection_check_scheduled = false; - self.checkIntersections() catch |err| { - log.err(.frame, "frame.schedIntersectChecks", .{ .err = err, .type = self._type, .url = self.url }); - }; -} - -pub fn deliverIntersections(self: *Frame) void { - if (!self._intersection_delivery_scheduled) { - return; - } - self._intersection_delivery_scheduled = false; - - // Iterate backwards to handle observers that disconnect during their callback - var i = self._intersection_observers.items.len; - while (i > 0) { - i -= 1; - const observer = self._intersection_observers.items[i]; - observer.deliverEntries(self) catch |err| { - log.err(.frame, "frame.deliverIntersections", .{ .err = err, .type = self._type, .url = self.url }); - }; - } -} - -pub fn deliverMutations(self: *Frame) void { - if (!self._mutation_delivery_scheduled) { - return; - } - self._mutation_delivery_scheduled = false; - - self._mutation_delivery_depth += 1; - defer if (!self._mutation_delivery_scheduled) { - // reset the depth once nothing is left to be scheduled - self._mutation_delivery_depth = 0; - }; - - if (self._mutation_delivery_depth > 100) { - log.err(.frame, "frame.MutationLimit", .{ .type = self._type, .url = self.url }); - self._mutation_delivery_depth = 0; - return; - } - - // snapshot the pending slots to deliver. We'll deliver these AFTER the mutation - // but new pending slots that land during mutation should only be delivered - // on the microtask tick. - const slots = self.call_arena.dupe(*Element.Html.Slot, self._slots_pending_slotchange.keys()) catch |err| blk: { - log.err(.frame, "deliverMutations.slots", .{ .err = err, .type = self._type, .url = self.url }); - break :blk &.{}; - }; - self._slots_pending_slotchange.clearRetainingCapacity(); - - // We only deliver notifications for observers that have records BEFORE - // we started the delivery. So we need to snapshot this. Any observers which - // get records during this phase will only be processed on the next microtask tick. - var notify: std.ArrayList(*MutationObserver) = .empty; - var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first; - while (it) |node| : (it = node.next) { - const observer: *MutationObserver = @fieldParentPtr("node", node); - if (observer._pending_records.items.len == 0) { - continue; - } - notify.append(self.call_arena, observer) catch |err| { - log.err(.frame, "deliverMutations.notify", .{ .err = err, .type = self._type, .url = self.url }); - break; - }; - } - - for (notify.items) |observer| { - observer.deliverRecords(self) catch |err| { - log.err(.frame, "frame.deliverMutations", .{ .err = err, .type = self._type, .url = self.url }); - }; - } - - // slotchange events fire after the observer callbacks (spec step order) - for (slots) |slot| { - const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self._page) catch |err| { - log.err(.frame, "deliverSlotchange.init", .{ .err = err, .type = self._type, .url = self.url }); - continue; - }; - const target = slot.asNode().asEventTarget(); - self._event_manager.dispatch(target, event) catch |err| { - log.err(.frame, "deliverSlotchange.dispatch", .{ .err = err, .type = self._type, .url = self.url }); - }; - } -} - pub fn notifyNetworkIdle(self: *Frame) void { lp.assert(self._notified_network_idle == .done, "Frame.notifyNetworkIdle", .{}); self._session.notification.dispatch(.frame_network_idle, &.{ @@ -2171,1002 +2018,6 @@ pub fn adoptNodeTree(self: *Frame, node: *Node, old_owner: *Document, new_owner: } } -pub fn createElementNS(self: *Frame, namespace: Element.Namespace, name: []const u8, attribute_iterator: anytype) !*Node { - const from_parser = @TypeOf(attribute_iterator) == Parser.AttributeIterator; - - switch (namespace) { - .html => { - switch (name.len) { - 1 => switch (name[0]) { - 'p' => return self.createHtmlElementT( - Element.Html.Paragraph, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - 'a' => return self.createHtmlElementT( - Element.Html.Anchor, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - 'b' => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("b"), ._tag = .b }, - ), - 'i' => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("i"), ._tag = .i }, - ), - 'q' => return self.createHtmlElementT( - Element.Html.Quote, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("q"), ._tag = .quote }, - ), - 's' => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("s"), ._tag = .s }, - ), - else => {}, - }, - 2 => switch (@as(u16, @bitCast(name[0..2].*))) { - asUint("br") => return self.createHtmlElementT( - Element.Html.BR, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("ol") => return self.createHtmlElementT( - Element.Html.OL, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("ul") => return self.createHtmlElementT( - Element.Html.UL, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("li") => return self.createHtmlElementT( - Element.Html.LI, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("h1") => return self.createHtmlElementT( - Element.Html.Heading, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("h1"), ._tag = .h1 }, - ), - asUint("h2") => return self.createHtmlElementT( - Element.Html.Heading, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("h2"), ._tag = .h2 }, - ), - asUint("h3") => return self.createHtmlElementT( - Element.Html.Heading, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("h3"), ._tag = .h3 }, - ), - asUint("h4") => return self.createHtmlElementT( - Element.Html.Heading, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("h4"), ._tag = .h4 }, - ), - asUint("h5") => return self.createHtmlElementT( - Element.Html.Heading, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("h5"), ._tag = .h5 }, - ), - asUint("h6") => return self.createHtmlElementT( - Element.Html.Heading, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("h6"), ._tag = .h6 }, - ), - asUint("hr") => return self.createHtmlElementT( - Element.Html.HR, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("em") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("em"), ._tag = .em }, - ), - asUint("dd") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("dd"), ._tag = .dd }, - ), - asUint("dl") => return self.createHtmlElementT( - Element.Html.DList, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("dt") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("dt"), ._tag = .dt }, - ), - asUint("td") => return self.createHtmlElementT( - Element.Html.TableCell, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("td"), ._tag = .td }, - ), - asUint("th") => return self.createHtmlElementT( - Element.Html.TableCell, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("th"), ._tag = .th }, - ), - asUint("tr") => return self.createHtmlElementT( - Element.Html.TableRow, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - else => {}, - }, - 3 => switch (@as(u24, @bitCast(name[0..3].*))) { - asUint("div") => return self.createHtmlElementT( - Element.Html.Div, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("img") => return self.createHtmlElementT( - Element.Html.Image, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("nav") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("nav"), ._tag = .nav }, - ), - asUint("del") => return self.createHtmlElementT( - Element.Html.Mod, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("del"), ._tag = .del }, - ), - asUint("ins") => return self.createHtmlElementT( - Element.Html.Mod, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("ins"), ._tag = .ins }, - ), - asUint("col") => return self.createHtmlElementT( - Element.Html.TableCol, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("col"), ._tag = .col }, - ), - asUint("dir") => return self.createHtmlElementT( - Element.Html.Directory, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("map") => return self.createHtmlElementT( - Element.Html.Map, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("pre") => return self.createHtmlElementT( - Element.Html.Pre, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("sub") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("sub"), ._tag = .sub }, - ), - asUint("sup") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("sup"), ._tag = .sup }, - ), - asUint("dfn") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("dfn"), ._tag = .dfn }, - ), - else => {}, - }, - 4 => switch (@as(u32, @bitCast(name[0..4].*))) { - asUint("span") => return self.createHtmlElementT( - Element.Html.Span, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("meta") => return self.createHtmlElementT( - Element.Html.Meta, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("link") => return self.createHtmlElementT( - Element.Html.Link, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("slot") => return self.createHtmlElementT( - Element.Html.Slot, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("html") => return self.createHtmlElementT( - Element.Html.Html, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("head") => { - // Inject user-provided scripts. - const inject_scripts = self._session.inject_scripts; - const should_inject_scripts = from_parser and self._parse_mode == .document and inject_scripts.len > 0; - - if (should_inject_scripts) { - var ls: JS.Local.Scope = undefined; - self.js.localScope(&ls); - defer ls.deinit(); - - for (inject_scripts) |inject_script| { - var try_catch: JS.TryCatch = undefined; - try_catch.init(&ls.local); - defer try_catch.deinit(); - - ls.local.eval(inject_script, "inject_script") catch |err| { - const caught = try_catch.caughtOrError(self.call_arena, err); - log.err(.app, "inject script error", .{ .err = caught }); - }; - } - } - - return self.createHtmlElementT( - Element.Html.Head, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ); - }, - asUint("body") => return self.createHtmlElementT( - Element.Html.Body, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("form") => return self.createHtmlElementT( - Element.Html.Form, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("main") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("main"), ._tag = .main }, - ), - asUint("data") => return self.createHtmlElementT( - Element.Html.Data, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("base") => { - const n = try self.createHtmlElementT( - Element.Html.Base, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ); - - // If frames's base url is not already set, fill it with - // the base tag. - if (self.base_url == null) { - if (n.as(Element).getAttributeSafe(comptime .wrap("href"))) |href| { - self.base_url = try URL.resolve(self.arena, self.url, href, .{}); - } - } - - return n; - }, - asUint("menu") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("menu"), ._tag = .menu }, - ), - asUint("area") => return self.createHtmlElementT( - Element.Html.Area, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("font") => return self.createHtmlElementT( - Element.Html.Font, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("code") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("code"), ._tag = .code }, - ), - asUint("time") => return self.createHtmlElementT( - Element.Html.Time, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - else => {}, - }, - 5 => switch (@as(u40, @bitCast(name[0..5].*))) { - asUint("input") => return self.createHtmlElementT( - Element.Html.Input, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("style") => return self.createHtmlElementT( - Element.Html.Style, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("title") => return self.createHtmlElementT( - Element.Html.Title, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("embed") => return self.createHtmlElementT( - Element.Html.Embed, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("audio") => return self.createHtmlMediaElementT( - Element.Html.Media.Audio, - namespace, - attribute_iterator, - ), - asUint("video") => return self.createHtmlMediaElementT( - Element.Html.Media.Video, - namespace, - attribute_iterator, - ), - asUint("aside") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("aside"), ._tag = .aside }, - ), - asUint("label") => return self.createHtmlElementT( - Element.Html.Label, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("meter") => return self.createHtmlElementT( - Element.Html.Meter, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("param") => return self.createHtmlElementT( - Element.Html.Param, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("table") => return self.createHtmlElementT( - Element.Html.Table, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("thead") => return self.createHtmlElementT( - Element.Html.TableSection, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("thead"), ._tag = .thead }, - ), - asUint("tbody") => return self.createHtmlElementT( - Element.Html.TableSection, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("tbody"), ._tag = .tbody }, - ), - asUint("tfoot") => return self.createHtmlElementT( - Element.Html.TableSection, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("tfoot"), ._tag = .tfoot }, - ), - asUint("track") => return self.createHtmlElementT( - Element.Html.Track, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._kind = comptime .wrap("subtitles"), ._ready_state = .none }, - ), - else => {}, - }, - 6 => switch (@as(u48, @bitCast(name[0..6].*))) { - asUint("script") => return self.createHtmlElementT( - Element.Html.Script, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("button") => return self.createHtmlElementT( - Element.Html.Button, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("canvas") => return self.createHtmlElementT( - Element.Html.Canvas, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("dialog") => return self.createHtmlElementT( - Element.Html.Dialog, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("legend") => return self.createHtmlElementT( - Element.Html.Legend, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("object") => return self.createHtmlElementT( - Element.Html.Object, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("output") => return self.createHtmlElementT( - Element.Html.Output, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("source") => return self.createHtmlElementT( - Element.Html.Source, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("strong") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("strong"), ._tag = .strong }, - ), - asUint("header") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("header"), ._tag = .header }, - ), - asUint("footer") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("footer"), ._tag = .footer }, - ), - asUint("select") => return self.createHtmlElementT( - Element.Html.Select, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("option") => return self.createHtmlElementT( - Element.Html.Option, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("iframe") => return self.createHtmlElementT( - IFrame, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("figure") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("figure"), ._tag = .figure }, - ), - asUint("hgroup") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("hgroup"), ._tag = .hgroup }, - ), - else => {}, - }, - 7 => switch (@as(u56, @bitCast(name[0..7].*))) { - asUint("section") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("section"), ._tag = .section }, - ), - asUint("article") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("article"), ._tag = .article }, - ), - asUint("details") => return self.createHtmlElementT( - Element.Html.Details, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("summary") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("summary"), ._tag = .summary }, - ), - asUint("caption") => return self.createHtmlElementT( - Element.Html.TableCaption, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("marquee") => return self.createHtmlElementT( - Element.Html.Marquee, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("address") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("address"), ._tag = .address }, - ), - asUint("picture") => return self.createHtmlElementT( - Element.Html.Picture, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - else => {}, - }, - 8 => switch (@as(u64, @bitCast(name[0..8].*))) { - asUint("textarea") => return self.createHtmlElementT( - Element.Html.TextArea, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("template") => return self.createHtmlElementT( - Element.Html.Template, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._content = undefined }, - ), - asUint("colgroup") => return self.createHtmlElementT( - Element.Html.TableCol, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("colgroup"), ._tag = .colgroup }, - ), - asUint("fieldset") => return self.createHtmlElementT( - Element.Html.FieldSet, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("frameset") => { - if (comptime from_parser) { - log.warn(.not_implemented, "framset", .{ .note = "... in html is not handled properly" }); - } - return self.createHtmlElementT( - Element.Html.FrameSet, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ); - }, - asUint("optgroup") => return self.createHtmlElementT( - Element.Html.OptGroup, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("progress") => return self.createHtmlElementT( - Element.Html.Progress, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("datalist") => return self.createHtmlElementT( - Element.Html.DataList, - namespace, - attribute_iterator, - .{ ._proto = undefined }, - ), - asUint("noscript") => return self.createHtmlElementT( - Element.Html.Generic, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("noscript"), ._tag = .noscript }, - ), - else => {}, - }, - 10 => switch (@as(u80, @bitCast(name[0..10].*))) { - asUint("blockquote") => return self.createHtmlElementT( - Element.Html.Quote, - namespace, - attribute_iterator, - .{ ._proto = undefined, ._tag_name = comptime .wrap("blockquote"), ._tag = .blockquote }, - ), - else => {}, - }, - else => {}, - } - const tag_name = try String.init(self.arena, name, .{}); - - // Check if this is a custom element (must have hyphen for HTML namespace) - const has_hyphen = std.mem.indexOfScalar(u8, name, '-') != null; - if (has_hyphen and namespace == .html) { - const definition = self.window._custom_elements._definitions.get(name); - const node = try self.createHtmlElementT(Element.Html.Custom, namespace, attribute_iterator, .{ - ._proto = undefined, - ._tag_name = tag_name, - ._definition = definition, - }); - - const def = definition orelse { - const element = node.as(Element); - const custom = element.is(Element.Html.Custom).?; - try self._undefined_custom_elements.append(self.arena, custom); - return node; - }; - - // Save and restore upgrading element to allow nested createElement calls - const prev_upgrading = self._upgrading_element; - self._upgrading_element = node; - defer self._upgrading_element = prev_upgrading; - - var ls: JS.Local.Scope = undefined; - self.js.localScope(&ls); - defer ls.deinit(); - - if (from_parser) { - // There are some things custom elements aren't allowed to do - // when we're parsing. - self.document._throw_on_dynamic_markup_insertion_counter += 1; - } - defer if (from_parser) { - self.document._throw_on_dynamic_markup_insertion_counter -= 1; - }; - - var caught: JS.TryCatch.Caught = undefined; - _ = ls.toLocal(def.constructor).newInstance(&caught) catch |err| { - log.warn(.js, "custom element constructor", .{ .name = name, .err = err, .caught = caught, .type = self._type, .url = self.url }); - return node; - }; - - // After constructor runs, invoke attributeChangedCallback for initial attributes - const element = node.as(Element); - if (element._attributes) |attributes| { - var it = attributes.iterator(); - while (it.next()) |attr| { - Element.Html.Custom.enqueueAttributeChangedCallbackOnElement( - element, - attr._name, - null, // old_value is null for initial attributes - attr._value, - null, - self, - ); - } - } - - return node; - } - - return self.createHtmlElementT(Element.Html.Unknown, namespace, attribute_iterator, .{ ._proto = undefined, ._tag_name = tag_name }); - }, - .svg => { - const tag_name = try String.init(self.arena, name, .{}); - if (std.ascii.eqlIgnoreCase(name, "svg")) { - return self.createSvgElementT(Element.Svg, name, attribute_iterator, .{ - ._proto = undefined, - ._type = .svg, - ._tag_name = tag_name, - }); - } - - // Other SVG elements (rect, circle, text, g, etc.) - const lower = std.ascii.lowerString(&self.buf, name); - const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown; - return self.createSvgElementT(Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag }); - }, - else => { - const tag_name = try String.init(self.arena, name, .{}); - return self.createHtmlElementT(Element.Html.Unknown, namespace, attribute_iterator, .{ ._proto = undefined, ._tag_name = tag_name }); - }, - } -} - -fn createHtmlElementT(self: *Frame, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype, html_element: E) !*Node { - const html_element_ptr = try self._factory.htmlElement(html_element); - const element = html_element_ptr.asElement(); - element._namespace = namespace; - try self.populateElementAttributes(element, attribute_iterator); - - // Check for customized built-in element via "is" attribute - try Element.Html.Custom.checkAndAttachBuiltIn(element, self); - - const node = element.asNode(); - if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) { - @call(.auto, @field(E.Build, "created"), .{ node, self }) catch |err| { - log.err(.frame, "build.created", .{ .tag = node.getNodeName(&self.buf), .err = err, .type = self._type, .url = self.url }); - return err; - }; - } - return node; -} - -fn createHtmlMediaElementT(self: *Frame, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype) !*Node { - const media_element = try self._factory.htmlMediaElement(E{ ._proto = undefined }); - const element = media_element.asElement(); - element._namespace = namespace; - try self.populateElementAttributes(element, attribute_iterator); - return element.asNode(); -} - -fn createSvgElementT(self: *Frame, comptime E: type, tag_name: []const u8, attribute_iterator: anytype, svg_element: E) !*Node { - const svg_element_ptr = try self._factory.svgElement(tag_name, svg_element); - var element = svg_element_ptr.asElement(); - element._namespace = .svg; - try self.populateElementAttributes(element, attribute_iterator); - return element.asNode(); -} - -fn populateElementAttributes(self: *Frame, element: *Element, list: anytype) !void { - if (@TypeOf(list) == ?*Element.Attribute.List) { - // from cloneNode - - var existing = list orelse return; - - var attributes = try self.arena.create(Element.Attribute.List); - attributes.* = .{ - .normalize = existing.normalize, - }; - - var it = existing.iterator(); - while (it.next()) |attr| { - try attributes.putNew(attr._name.str(), attr._value.str(), self); - } - element._attributes = attributes; - return; - } - - // from the parser - if (@TypeOf(list) == @TypeOf(null) or list.count() == 0) { - return; - } - var attributes = try element.createAttributeList(self); - while (list.next()) |attr| { - try attributes.putNew(attr.name.local.slice(), attr.value.slice(), self); - } -} - -// Called when `new MyElement()` is invoked directly in JS (not via the -// customElements.define/upgrade path). `new_target` is the constructor -// function that was used with `new`. We find the matching definition in the -// registry by function identity and allocate a detached Custom element with -// the registered tag name. -pub fn constructCustomElement(self: *Frame, new_target: JS.Function) !*Element { - var it = self.window._custom_elements._definitions.iterator(); - const definition = while (it.next()) |entry| { - if (entry.value_ptr.*.constructor.isEqual(new_target)) { - break entry.value_ptr.*; - } - } else return error.IllegalConstructor; - - // Customized built-ins (`class Foo extends HTMLDivElement`, etc.) would - // need to allocate the extended HTML type rather than Custom. Not yet - // supported via direct `new` — upgrade path still works for those. - if (definition.isCustomizedBuiltIn()) { - return error.IllegalConstructor; - } - - const tag_name = try String.init(self.arena, definition.name, .{}); - const node = try self.createHtmlElementT(Element.Html.Custom, .html, @as(?*Element.Attribute.List, null), .{ - ._proto = undefined, - ._tag_name = tag_name, - ._definition = definition, - }); - return node.as(Element); -} - -pub fn createTextNode(self: *Frame, text: []const u8) !*Node { - const cd = try self._factory.node(CData{ - ._proto = undefined, - ._type = .{ .text = .{ - ._proto = undefined, - } }, - ._data = try self.dupeSSO(text), - }); - cd._type.text._proto = cd; - return cd.asNode(); -} - -pub fn createComment(self: *Frame, text: []const u8) !*Node { - const cd = try self._factory.node(CData{ - ._proto = undefined, - ._type = .{ .comment = .{ - ._proto = undefined, - } }, - ._data = try self.dupeSSO(text), - }); - cd._type.comment._proto = cd; - return cd.asNode(); -} - -pub fn createCDATASection(self: *Frame, data: []const u8) !*Node { - // Validate that the data doesn't contain "]]>" - if (std.mem.indexOf(u8, data, "]]>") != null) { - return error.InvalidCharacterError; - } - - // First allocate the Text node separately - const text_node = try self._factory.create(CData.Text{ - ._proto = undefined, - }); - - // Then create the CData with cdata_section variant - const cd = try self._factory.node(CData{ - ._proto = undefined, - ._type = .{ .cdata_section = .{ - ._proto = text_node, - } }, - ._data = try self.dupeSSO(data), - }); - - // Set up the back pointer from Text to CData - text_node._proto = cd; - - return cd.asNode(); -} - -pub fn createProcessingInstruction(self: *Frame, target: []const u8, data: []const u8) !*Node { - // Validate neither target nor data contain "?>" - if (std.mem.indexOf(u8, target, "?>") != null) { - return error.InvalidCharacterError; - } - if (std.mem.indexOf(u8, data, "?>") != null) { - return error.InvalidCharacterError; - } - - // Validate target follows XML Name production - try validateXmlName(target); - - const owned_target = try self.dupeString(target); - - const pi = try self._factory.create(CData.ProcessingInstruction{ - ._proto = undefined, - ._target = owned_target, - }); - - const cd = try self._factory.node(CData{ - ._proto = undefined, - ._type = .{ .processing_instruction = pi }, - ._data = try self.dupeSSO(data), - }); - - // Set up the back pointer from ProcessingInstruction to CData - pi._proto = cd; - - return cd.asNode(); -} - -/// Validate a string against the XML Name production. -/// https://www.w3.org/TR/xml/#NT-Name -fn validateXmlName(name: []const u8) !void { - if (name.len == 0) return error.InvalidCharacterError; - - var i: usize = 0; - - // First character must be a NameStartChar. - const first_len = std.unicode.utf8ByteSequenceLength(name[0]) catch - return error.InvalidCharacterError; - if (first_len > name.len) return error.InvalidCharacterError; - const first_cp = std.unicode.utf8Decode(name[0..][0..first_len]) catch - return error.InvalidCharacterError; - if (!isXmlNameStartChar(first_cp)) return error.InvalidCharacterError; - i = first_len; - - // Subsequent characters must be NameChars. - while (i < name.len) { - const cp_len = std.unicode.utf8ByteSequenceLength(name[i]) catch - return error.InvalidCharacterError; - if (i + cp_len > name.len) return error.InvalidCharacterError; - const cp = std.unicode.utf8Decode(name[i..][0..cp_len]) catch - return error.InvalidCharacterError; - if (!isXmlNameChar(cp)) return error.InvalidCharacterError; - i += cp_len; - } -} - -fn isXmlNameStartChar(c: u21) bool { - return c == ':' or - (c >= 'A' and c <= 'Z') or - c == '_' or - (c >= 'a' and c <= 'z') or - (c >= 0xC0 and c <= 0xD6) or - (c >= 0xD8 and c <= 0xF6) or - (c >= 0xF8 and c <= 0x2FF) or - (c >= 0x370 and c <= 0x37D) or - (c >= 0x37F and c <= 0x1FFF) or - (c >= 0x200C and c <= 0x200D) or - (c >= 0x2070 and c <= 0x218F) or - (c >= 0x2C00 and c <= 0x2FEF) or - (c >= 0x3001 and c <= 0xD7FF) or - (c >= 0xF900 and c <= 0xFDCF) or - (c >= 0xFDF0 and c <= 0xFFFD) or - (c >= 0x10000 and c <= 0xEFFFF); -} - -fn isXmlNameChar(c: u21) bool { - return isXmlNameStartChar(c) or - c == '-' or - c == '.' or - (c >= '0' and c <= '9') or - c == 0xB7 or - (c >= 0x300 and c <= 0x36F) or - (c >= 0x203F and c <= 0x2040); -} - pub fn dupeString(self: *Frame, value: []const u8) ![]const u8 { if (String.intern(value)) |v| { return v; @@ -3231,9 +2082,9 @@ pub fn removeNode(self: *Frame, parent: *Node, child: *Node, opts: RemoveNodeOpt slotting.removalSteps(parent, child, self); - if (self.hasMutationObservers()) { + if (observers.hasMutationObservers(self)) { const removed = [_]*Node{child}; - self.childListChange(parent, &.{}, &removed, previous_sibling, next_sibling); + observers.notifyChildListChange(self, parent, &.{}, &removed, previous_sibling, next_sibling); } if (opts.will_be_reconnected) { @@ -3416,11 +2267,11 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No } // Notify mutation observers about childList change - if (self.hasMutationObservers()) { + if (observers.hasMutationObservers(self)) { const previous_sibling = child.previousSibling(); const next_sibling = child.nextSibling(); const added = [_]*Node{child}; - self.childListChange(parent, &added, &.{}, previous_sibling, next_sibling); + observers.notifyChildListChange(self, parent, &added, &.{}, previous_sibling, next_sibling); } } @@ -3485,13 +2336,7 @@ pub fn attributeChange(self: *Frame, element: *Element, name: String, value: Str Element.Html.Custom.enqueueAttributeChangedCallbackOnElement(element, name, old_value, value, null, self); - var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first; - while (it) |node| : (it = node.next) { - const observer: *MutationObserver = @fieldParentPtr("node", node); - observer.notifyAttributeChange(element, name, old_value, self) catch |err| { - log.err(.frame, "attributeChange.notifyObserver", .{ .err = err, .type = self._type, .url = self.url }); - }; - } + observers.notifyAttributeChange(self, element, name, old_value); // Handle slot assignment changes if (name.eql(comptime .wrap("slot"))) { @@ -3515,13 +2360,7 @@ pub fn attributeRemove(self: *Frame, element: *Element, name: String, old_value: Element.Html.Custom.enqueueAttributeChangedCallbackOnElement(element, name, old_value, null, null, self); - var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first; - while (it) |node| : (it = node.next) { - const observer: *MutationObserver = @fieldParentPtr("node", node); - observer.notifyAttributeChange(element, name, old_value, self) catch |err| { - log.err(.frame, "attributeRemove.notifyObserver", .{ .err = err, .type = self._type, .url = self.url }); - }; - } + observers.notifyAttributeChange(self, element, name, old_value); // Handle slot assignment changes if (name.eql(comptime .wrap("slot"))) { @@ -3540,15 +2379,11 @@ pub fn signalSlotChange(self: *Frame, slot: *Element.Html.Slot) void { log.err(.frame, "signalSlotChange.put", .{ .err = err, .type = self._type, .url = self.url }); return; }; - self.scheduleMutationDelivery() catch |err| { + observers.scheduleMutationDelivery(self) catch |err| { log.err(.frame, "signalSlotChange.schedule", .{ .err = err, .type = self._type, .url = self.url }); }; } -pub fn hasMutationObservers(self: *const Frame) bool { - return self._mutation_observers.first != null; -} - pub fn getCustomizedBuiltInDefinition(self: *Frame, element: *Element) ?*CustomElementDefinition { return self._customized_builtin_definitions.get(element); } @@ -3557,47 +2392,6 @@ pub fn setCustomizedBuiltInDefinition(self: *Frame, element: *Element, definitio try self._customized_builtin_definitions.put(self.arena, element, definition); } -pub fn characterDataChange( - self: *Frame, - target: *Node, - old_value: String, -) void { - var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first; - while (it) |node| : (it = node.next) { - const observer: *MutationObserver = @fieldParentPtr("node", node); - observer.notifyCharacterDataChange(target, old_value, self) catch |err| { - log.err(.frame, "cdataChange.notifyObserver", .{ .err = err, .type = self._type, .url = self.url }); - }; - } -} - -pub fn childListChange( - self: *Frame, - target: *Node, - added_nodes: []const *Node, - removed_nodes: []const *Node, - previous_sibling: ?*Node, - next_sibling: ?*Node, -) void { - // Filter out HTML wrapper element during fragment parsing (html5ever quirk) - if (self._parse_mode == .fragment and added_nodes.len == 1) { - if (added_nodes[0].is(Element.Html.Html) != null) { - // This is the temporary HTML wrapper, added by html5ever - // that will be unwrapped, see: - // https://github.com/servo/html5ever/issues/583 - return; - } - } - - var it: ?*std.DoublyLinkedList.Node = self._mutation_observers.first; - while (it) |node| : (it = node.next) { - const observer: *MutationObserver = @fieldParentPtr("node", node); - observer.notifyChildListChange(target, added_nodes, removed_nodes, previous_sibling, next_sibling, self) catch |err| { - log.err(.frame, "childListChange.notifyObserver", .{ .err = err, .type = self._type, .url = self.url }); - }; - } -} - // --- Live range update methods (DOM spec §4.2.3, §4.2.4, §4.7, §4.8) --- /// Update all live ranges after a replaceData mutation on a CharacterData node. @@ -3706,7 +2500,7 @@ fn parseHtmlAsChildrenInner(self: *Frame, node: *Node, html: []const u8, opts: F } node._children = first._children; - if (self.hasMutationObservers()) { + if (observers.hasMutationObservers(self)) { var it = node.childrenIterator(); while (it.next()) |child| { child._parent = node; @@ -3714,7 +2508,7 @@ fn parseHtmlAsChildrenInner(self: *Frame, node: *Node, html: []const u8, opts: F const previous_sibling = child.previousSibling(); const next_sibling = child.nextSibling(); const added = [_]*Node{child}; - self.childListChange(node, &added, &.{}, previous_sibling, next_sibling); + observers.notifyChildListChange(self, node, &added, &.{}, previous_sibling, next_sibling); } } else { var it = node.childrenIterator(); @@ -4012,425 +2806,6 @@ fn findFrameByName(frame: *Frame, name: []const u8) ?*Frame { return null; } -// DOM MouseEvent.button values. -// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button -pub const mouse_button = struct { - pub const main: i32 = 0; // left - pub const auxiliary: i32 = 1; // middle - pub const secondary: i32 = 2; // right - pub const fourth: i32 = 3; // back - pub const fifth: i32 = 4; // forward -}; - -// Dispatch a single trusted mouse event of the given type on `target`, carrying -// the pressed button and pointer position. `detail` is the click count (used for -// click/dblclick); 0 for events where it does not apply. -fn dispatchMouseEventOn(self: *Frame, target: *Element, comptime typ: []const u8, x: f64, y: f64, button: i32, detail: u32) !void { - const event: *MouseEvent = try .initTrusted(comptime .wrap(typ), .{ - .bubbles = true, - .cancelable = true, - .composed = true, - .clientX = x, - .clientY = y, - .button = button, - .detail = detail, - }, self); - try self._event_manager.dispatch(target.asEventTarget(), event.asEvent()); -} - -pub fn triggerMousePress(self: *Frame, x: f64, y: f64, button: i32) !void { - const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return; - if (comptime IS_DEBUG) { - log.debug(.frame, "frame mouse press", .{ - .url = self.url, - .node = target, - .x = x, - .y = y, - .button = button, - .type = self._type, - }); - } - try self.dispatchMouseEventOn(target, "mousedown", x, y, button, 0); -} - -pub fn triggerMouseMove(self: *Frame, x: f64, y: f64) !void { - const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return; - if (comptime IS_DEBUG) { - log.debug(.frame, "frame mouse move", .{ - .url = self.url, - .node = target, - .x = x, - .y = y, - .type = self._type, - }); - } - - const move_event: *MouseEvent = try .initTrusted(comptime .wrap("mousemove"), .{ - .bubbles = true, - .cancelable = true, - .composed = true, - .clientX = x, - .clientY = y, - }, self); - try self._event_manager.dispatch(target.asEventTarget(), move_event.asEvent()); - - const over_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseover"), .{ - .bubbles = true, - .cancelable = true, - .composed = true, - .clientX = x, - .clientY = y, - }, self); - try self._event_manager.dispatch(target.asEventTarget(), over_event.asEvent()); - - const enter_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseenter"), .{ - .composed = true, - .clientX = x, - .clientY = y, - }, self); - try self._event_manager.dispatch(target.asEventTarget(), enter_event.asEvent()); -} - -pub fn triggerMouseRelease(self: *Frame, x: f64, y: f64, button: i32, click_count: i32) !void { - const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return; - if (comptime IS_DEBUG) { - log.debug(.frame, "frame mouse release", .{ - .url = self.url, - .node = target, - .x = x, - .y = y, - .button = button, - .type = self._type, - }); - } - - const detail: u32 = if (click_count > 0) @intCast(click_count) else 1; - - try self.dispatchMouseEventOn(target, "mouseup", x, y, button, detail); - - // After mouseup, the activation event depends on the button. - switch (button) { - mouse_button.main => { - try self.dispatchMouseEventOn(target, "click", x, y, button, detail); - // A second click in quick succession also fires dblclick. - if (click_count == 2) { - try self.dispatchMouseEventOn(target, "dblclick", x, y, button, detail); - } - }, - mouse_button.auxiliary => try self.dispatchMouseEventOn(target, "auxclick", x, y, button, detail), - mouse_button.secondary => try self.dispatchMouseEventOn(target, "contextmenu", x, y, button, detail), - else => {}, - } -} - -pub fn triggerMouseWheel(self: *Frame, x: f64, y: f64, delta_x: f64, delta_y: f64) !void { - const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return; - if (comptime IS_DEBUG) { - log.debug(.frame, "frame mouse wheel", .{ - .url = self.url, - .node = target, - .x = x, - .y = y, - .delta_x = delta_x, - .delta_y = delta_y, - .type = self._type, - }); - } - - const wheel_event: *WheelEvent = try .initTrusted("wheel", .{ - .bubbles = true, - .cancelable = true, - .composed = true, - .clientX = x, - .clientY = y, - .deltaX = delta_x, - .deltaY = delta_y, - }, self); - - // Keep the event alive past dispatch so we can read _prevent_default. - wheel_event.asEvent().acquireRef(); - defer _ = wheel_event.asEvent().releaseRef(self._page); - try self._event_manager.dispatch(target.asEventTarget(), wheel_event.asEvent()); - - if (wheel_event.asEvent()._prevent_default) { - return; - } - - // Apply the scroll and fire a trusted scroll event, mirroring WebDriver wheel. - // CDP deltas are untrusted, so guard NaN and saturate the addition. - const new_left: i32 = @as(i32, @intCast(target.getScrollLeft(self))) +| deltaToScroll(delta_x); - const new_top: i32 = @as(i32, @intCast(target.getScrollTop(self))) +| deltaToScroll(delta_y); - try target.setScrollLeft(new_left, self); - try target.setScrollTop(new_top, self); - - const scroll_event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, self._page); - try self._event_manager.dispatch(target.asEventTarget(), scroll_event); -} - -fn deltaToScroll(d: f64) i32 { - if (std.math.isNan(d)) return 0; - return @intFromFloat(std.math.clamp(d, std.math.minInt(i32), std.math.maxInt(i32))); -} - -// callback when the "click" event reaches the frame. -pub fn handleClick(self: *Frame, target: *Node) !void { - // TODO: Also support elements when implement - const element = target.is(Element) orelse return; - const html_element = element.is(Element.Html) orelse return; - - switch (html_element._type) { - .anchor => |anchor| { - const href = element.getAttributeSafe(comptime .wrap("href")) orelse return; - if (href.len == 0) { - return; - } - - if (std.mem.startsWith(u8, href, "javascript:")) { - return; - } - - if (try element.hasAttribute(comptime .wrap("download"), self)) { - log.warn(.browser, "a.download", .{ .type = self._type, .url = self.url }); - return; - } - - const target_frame = blk: { - const target_name = anchor.getTarget(); - if (target_name.len == 0) { - break :blk target.ownerFrame(self); - } - break :blk self.resolveTargetFrame(target_name) orelse { - log.warn(.not_implemented, "target", .{ .type = self._type, .url = self.url, .target = target_name }); - return; - }; - }; - - try element.focus(self); - try self.scheduleNavigation(href, .{ - .reason = .script, - .kind = .{ .push = null }, - }, .{ .anchor = target_frame }); - }, - .input => |input| { - try element.focus(self); - // Per HTML §4.10.18.6.4 "Image Button state (type=image)", clicking an - // image button submits its form. The form-data set already gets the - // submitter's coordinate fields appended via FormData.collectForm - // (see src/browser/webapi/net/FormData.zig). - if (input._input_type == .submit or input._input_type == .image) { - return self.submitForm(element, input.getForm(self), .{}); - } - }, - .button => |button| { - try element.focus(self); - if (std.mem.eql(u8, button.getType(), "submit")) { - return self.submitForm(element, button.getForm(self), .{}); - } - }, - .select, .textarea => try element.focus(self), - .label => |label| { - // Per HTML §4.10.4 "The label element", a label's activation - // behavior is to run the synthetic click activation steps on the - // labeled control. Mirrors Chrome's HTMLLabelElement::DefaultEventHandler. - const control = label.getControl(self) orelse return; - const control_html = control.is(Element.Html) orelse return; - try control_html.click(self); - }, - .generic => |generic| { - switch (generic._tag) { - .summary => { - const parent_el = target.parentElement() orelse return; - const details = parent_el.is(Element.Html.Details) orelse return; - var maybe_prev = element.previousElementSibling(); - while (maybe_prev) |prev| { - if (prev.getTag() == .summary) { - // we found a summary element before the clicked one - return; - } - maybe_prev = prev.previousElementSibling(); - } - try details.setOpen(!details.getOpen(), self); - }, - else => {}, - } - }, - else => {}, - } -} - -pub fn triggerKeyboard(self: *Frame, keyboard_event: *KeyboardEvent) !void { - const event = keyboard_event.asEvent(); - // Dispatch to the effective active element. When nothing is explicitly - // focused this resolves to (matching `document.activeElement`), so - // the keydown still fires and its default action — e.g. sequential focus - // navigation on Tab — can run. - const element = self.window._document.getActiveElement() orelse { - event.deinit(self._page); - return; - }; - - if (comptime IS_DEBUG) { - log.debug(.frame, "frame keydown", .{ - .url = self.url, - .node = element, - .key = keyboard_event._key, - .type = self._type, - }); - } - try self._event_manager.dispatch(element.asEventTarget(), event); -} - -pub fn handleKeydown(self: *Frame, target: *Node, event: *Event) !void { - const keyboard_event = event.is(KeyboardEvent) orelse return; - const key = keyboard_event.getKey(); - - if (key == .Dead) { - return; - } - - if (key == .Tab) { - // tab -> forward, shift+tab -> backwards - return self.moveFocus(keyboard_event.getShiftKey() == false); - } - - if (target.is(Element.Html.Input)) |input| { - if (key == .Enter) { - return self.submitForm(input.asElement(), input.getForm(self), .{}); - } - - // Don't handle text input for radio/checkbox - const input_type = input._input_type; - if (input_type == .radio or input_type == .checkbox) { - return; - } - - // Handle printable characters - if (key.isPrintable()) { - try input.innerInsert(key.asString(), self); - } - return; - } - - if (target.is(Element.Html.TextArea)) |textarea| { - // zig fmt: off - const append = - if (key == .Enter) "\n" - else if (key.isPrintable()) key.asString() - else return - ; - // zig fmt: on - return textarea.innerInsert(append, self); - } -} - -// Sequential focus navigation: move `document.activeElement` to the next (Tab) -// or previous (Shift+Tab) focusable element, firing the usual blur/focus events -// via `Element.focus`. The order is fully determined by tabindex + document -// position, so no layout is needed: -// 1. elements with a positive tabindex, in ascending tabindex order; -// 2. then elements with tabindex 0 (or a natively-focusable default), in -// document order. -// Ties within a group break on document order, and Tab wraps around at the ends. -// https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation -fn moveFocus(self: *Frame, forward: bool) !void { - const document = self.document; - const current = document._active_element; - - const current_tab_index = blk: { - const cur = current orelse break :blk 0; - const current_html = cur.is(Element.Html) orelse break :blk 0; - break :blk current_html.getTabIndex(); - }; - - // Single document-order pass tracking two candidates: - // edge — the global first (forward) / last (backward) focusable element, - // used to wrap around when `current` is at an end, or as the - // landing spot when nothing is focused yet. - // chosen — the closest focusable element strictly past `current` in the - // travel direction. - var edge: ?*Element = null; - var edge_tab_index: i32 = 0; - - var chosen: ?*Element = null; - var chosen_tab_index: i32 = 0; - - var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(document.asNode(), .{}); - while (tw.next()) |candidate| { - if (candidate.isDisabled()) { - continue; - } - if (candidate.is(Element.Html) == null) { - continue; - } - - const candidate_tab_index = blk: { - if (candidate.getAttributeSafe(comptime .wrap("tabindex"))) |attr| { - if (Element.Html.parseInteger(attr)) |tab_index| { - if (tab_index < 0) { - continue; - } - break :blk tab_index; - } - break :blk 0; - } - - // no tab index, maybe this item isn't focusable.. - const focusable = switch (candidate.getTag()) { - .button, .select, .textarea, .iframe => true, - .input => candidate.as(Element.Html.Input)._input_type != .hidden, - .anchor, .area => candidate.getAttributeSafe(comptime .wrap("href")) != null, - else => false, - }; - if (focusable == false) { - continue; - } - - break :blk 0; - }; - - if (edge == null or focusOrderBefore(candidate, candidate_tab_index, edge.?, edge_tab_index) == forward) { - edge = candidate; - edge_tab_index = candidate_tab_index; - } - - const cur = current orelse continue; - - if (candidate == cur) { - continue; - } - - const past = if (forward) focusOrderBefore(cur, current_tab_index, candidate, candidate_tab_index) else focusOrderBefore(candidate, candidate_tab_index, cur, current_tab_index); - if (!past) { - continue; - } - if (chosen == null or focusOrderBefore(candidate, candidate_tab_index, chosen.?, chosen_tab_index) == forward) { - chosen = candidate; - chosen_tab_index = candidate_tab_index; - } - } - - const next = chosen orelse edge orelse return; - try next.focus(self); -} - -// Orders two focusable elements by sequential focus navigation order: positive -// tabindex first (ascending), then tabindex 0, ties broken by document order. -fn focusOrderBefore(a: *Element, a_tab_index: i32, b: *Element, b_tab_index: i32) bool { - if (a_tab_index == b_tab_index) { - // Equal tabindex → document order: `a` precedes `b` when `b` follows `a`. - const FOLLOWING: u16 = 0x04; - return (a.asNode().compareDocumentPosition(b.asNode()) & FOLLOWING) != 0; - } - - const group_a: u8 = if (a_tab_index > 0) 0 else 1; - const group_b: u8 = if (b_tab_index > 0) 0 else 1; - if (group_a != group_b) { - return group_a < group_b; - } - - return a_tab_index < b_tab_index; -} - const SubmitFormOpts = struct { fire_event: bool = true, }; @@ -4612,37 +2987,6 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For return self.scheduleNavigationWithArena(arena, action, opts, .{ .form = target_frame }); } -// insertText is a shortcut to insert text into the active element. -pub fn insertText(self: *Frame, v: []const u8) !void { - const html_element = self.document._active_element orelse return; - - if (html_element.is(Element.Html.Input)) |input| { - const input_type = input._input_type; - if (input_type == .radio or input_type == .checkbox) { - return; - } - - return input.innerInsert(v, self); - } - - if (html_element.is(Element.Html.TextArea)) |textarea| { - return textarea.innerInsert(v, self); - } -} - -fn asUint(comptime string: anytype) std.meta.Int( - .unsigned, - @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0 -) { - const byteLength = @sizeOf(@TypeOf(string.*)) - 1; - const expectedType = *const [byteLength:0]u8; - if (@TypeOf(string) != expectedType) { - @compileError("expected : " ++ @typeName(expectedType) ++ ", got: " ++ @typeName(@TypeOf(string))); - } - - return @bitCast(@as(*const [byteLength]u8, string).*); -} - const testing = @import("../testing.zig"); test "WebApi: Frame" { const filter: testing.LogFilter = .init(&.{.http}); diff --git a/src/browser/frame/node_factory.zig b/src/browser/frame/node_factory.zig new file mode 100644 index 000000000..43412acb8 --- /dev/null +++ b/src/browser/frame/node_factory.zig @@ -0,0 +1,1148 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Node creation for a frame: the createElementNS comptime tag dispatch and its +// element-building helpers, plus the Text/Comment/CDATASection/ProcessingInstruction +// factories and XML Name validation. All allocate through the frame's Factory and +// arenas; these functions operate on a *Frame. + +const std = @import("std"); +const lp = @import("lightpanda"); + +const JS = @import("../js/js.zig"); +const URL = @import("../URL.zig"); +const Frame = @import("../Frame.zig"); +const Parser = @import("../parser/Parser.zig"); + +const Node = @import("../webapi/Node.zig"); +const CData = @import("../webapi/CData.zig"); +const Element = @import("../webapi/Element.zig"); + +const log = lp.log; +const String = lp.String; +const IFrame = Element.Html.IFrame; + +pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []const u8, attribute_iterator: anytype) !*Node { + const from_parser = @TypeOf(attribute_iterator) == Parser.AttributeIterator; + + switch (namespace) { + .html => { + switch (name.len) { + 1 => switch (name[0]) { + 'p' => return createHtmlElementT( + frame, + Element.Html.Paragraph, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + 'a' => return createHtmlElementT( + frame, + Element.Html.Anchor, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + 'b' => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("b"), ._tag = .b }, + ), + 'i' => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("i"), ._tag = .i }, + ), + 'q' => return createHtmlElementT( + frame, + Element.Html.Quote, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("q"), ._tag = .quote }, + ), + 's' => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("s"), ._tag = .s }, + ), + else => {}, + }, + 2 => switch (@as(u16, @bitCast(name[0..2].*))) { + asUint("br") => return createHtmlElementT( + frame, + Element.Html.BR, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("ol") => return createHtmlElementT( + frame, + Element.Html.OL, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("ul") => return createHtmlElementT( + frame, + Element.Html.UL, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("li") => return createHtmlElementT( + frame, + Element.Html.LI, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("h1") => return createHtmlElementT( + frame, + Element.Html.Heading, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("h1"), ._tag = .h1 }, + ), + asUint("h2") => return createHtmlElementT( + frame, + Element.Html.Heading, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("h2"), ._tag = .h2 }, + ), + asUint("h3") => return createHtmlElementT( + frame, + Element.Html.Heading, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("h3"), ._tag = .h3 }, + ), + asUint("h4") => return createHtmlElementT( + frame, + Element.Html.Heading, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("h4"), ._tag = .h4 }, + ), + asUint("h5") => return createHtmlElementT( + frame, + Element.Html.Heading, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("h5"), ._tag = .h5 }, + ), + asUint("h6") => return createHtmlElementT( + frame, + Element.Html.Heading, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("h6"), ._tag = .h6 }, + ), + asUint("hr") => return createHtmlElementT( + frame, + Element.Html.HR, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("em") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("em"), ._tag = .em }, + ), + asUint("dd") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("dd"), ._tag = .dd }, + ), + asUint("dl") => return createHtmlElementT( + frame, + Element.Html.DList, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("dt") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("dt"), ._tag = .dt }, + ), + asUint("td") => return createHtmlElementT( + frame, + Element.Html.TableCell, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("td"), ._tag = .td }, + ), + asUint("th") => return createHtmlElementT( + frame, + Element.Html.TableCell, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("th"), ._tag = .th }, + ), + asUint("tr") => return createHtmlElementT( + frame, + Element.Html.TableRow, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + else => {}, + }, + 3 => switch (@as(u24, @bitCast(name[0..3].*))) { + asUint("div") => return createHtmlElementT( + frame, + Element.Html.Div, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("img") => return createHtmlElementT( + frame, + Element.Html.Image, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("nav") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("nav"), ._tag = .nav }, + ), + asUint("del") => return createHtmlElementT( + frame, + Element.Html.Mod, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("del"), ._tag = .del }, + ), + asUint("ins") => return createHtmlElementT( + frame, + Element.Html.Mod, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("ins"), ._tag = .ins }, + ), + asUint("col") => return createHtmlElementT( + frame, + Element.Html.TableCol, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("col"), ._tag = .col }, + ), + asUint("dir") => return createHtmlElementT( + frame, + Element.Html.Directory, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("map") => return createHtmlElementT( + frame, + Element.Html.Map, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("pre") => return createHtmlElementT( + frame, + Element.Html.Pre, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("sub") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("sub"), ._tag = .sub }, + ), + asUint("sup") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("sup"), ._tag = .sup }, + ), + asUint("dfn") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("dfn"), ._tag = .dfn }, + ), + else => {}, + }, + 4 => switch (@as(u32, @bitCast(name[0..4].*))) { + asUint("span") => return createHtmlElementT( + frame, + Element.Html.Span, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("meta") => return createHtmlElementT( + frame, + Element.Html.Meta, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("link") => return createHtmlElementT( + frame, + Element.Html.Link, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("slot") => return createHtmlElementT( + frame, + Element.Html.Slot, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("html") => return createHtmlElementT( + frame, + Element.Html.Html, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("head") => { + // Inject user-provided scripts. + const inject_scripts = frame._session.inject_scripts; + const should_inject_scripts = from_parser and frame._parse_mode == .document and inject_scripts.len > 0; + + if (should_inject_scripts) { + var ls: JS.Local.Scope = undefined; + frame.js.localScope(&ls); + defer ls.deinit(); + + for (inject_scripts) |inject_script| { + var try_catch: JS.TryCatch = undefined; + try_catch.init(&ls.local); + defer try_catch.deinit(); + + ls.local.eval(inject_script, "inject_script") catch |err| { + const caught = try_catch.caughtOrError(frame.call_arena, err); + log.err(.app, "inject script error", .{ .err = caught }); + }; + } + } + + return createHtmlElementT( + frame, + Element.Html.Head, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ); + }, + asUint("body") => return createHtmlElementT( + frame, + Element.Html.Body, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("form") => return createHtmlElementT( + frame, + Element.Html.Form, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("main") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("main"), ._tag = .main }, + ), + asUint("data") => return createHtmlElementT( + frame, + Element.Html.Data, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("base") => { + const n = try createHtmlElementT( + frame, + Element.Html.Base, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ); + + // If frames's base url is not already set, fill it with + // the base tag. + if (frame.base_url == null) { + if (n.as(Element).getAttributeSafe(comptime .wrap("href"))) |href| { + frame.base_url = try URL.resolve(frame.arena, frame.url, href, .{}); + } + } + + return n; + }, + asUint("menu") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("menu"), ._tag = .menu }, + ), + asUint("area") => return createHtmlElementT( + frame, + Element.Html.Area, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("font") => return createHtmlElementT( + frame, + Element.Html.Font, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("code") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("code"), ._tag = .code }, + ), + asUint("time") => return createHtmlElementT( + frame, + Element.Html.Time, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + else => {}, + }, + 5 => switch (@as(u40, @bitCast(name[0..5].*))) { + asUint("input") => return createHtmlElementT( + frame, + Element.Html.Input, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("style") => return createHtmlElementT( + frame, + Element.Html.Style, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("title") => return createHtmlElementT( + frame, + Element.Html.Title, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("embed") => return createHtmlElementT( + frame, + Element.Html.Embed, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("audio") => return createHtmlMediaElementT( + frame, + Element.Html.Media.Audio, + namespace, + attribute_iterator, + ), + asUint("video") => return createHtmlMediaElementT( + frame, + Element.Html.Media.Video, + namespace, + attribute_iterator, + ), + asUint("aside") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("aside"), ._tag = .aside }, + ), + asUint("label") => return createHtmlElementT( + frame, + Element.Html.Label, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("meter") => return createHtmlElementT( + frame, + Element.Html.Meter, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("param") => return createHtmlElementT( + frame, + Element.Html.Param, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("table") => return createHtmlElementT( + frame, + Element.Html.Table, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("thead") => return createHtmlElementT( + frame, + Element.Html.TableSection, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("thead"), ._tag = .thead }, + ), + asUint("tbody") => return createHtmlElementT( + frame, + Element.Html.TableSection, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("tbody"), ._tag = .tbody }, + ), + asUint("tfoot") => return createHtmlElementT( + frame, + Element.Html.TableSection, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("tfoot"), ._tag = .tfoot }, + ), + asUint("track") => return createHtmlElementT( + frame, + Element.Html.Track, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._kind = comptime .wrap("subtitles"), ._ready_state = .none }, + ), + else => {}, + }, + 6 => switch (@as(u48, @bitCast(name[0..6].*))) { + asUint("script") => return createHtmlElementT( + frame, + Element.Html.Script, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("button") => return createHtmlElementT( + frame, + Element.Html.Button, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("canvas") => return createHtmlElementT( + frame, + Element.Html.Canvas, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("dialog") => return createHtmlElementT( + frame, + Element.Html.Dialog, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("legend") => return createHtmlElementT( + frame, + Element.Html.Legend, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("object") => return createHtmlElementT( + frame, + Element.Html.Object, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("output") => return createHtmlElementT( + frame, + Element.Html.Output, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("source") => return createHtmlElementT( + frame, + Element.Html.Source, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("strong") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("strong"), ._tag = .strong }, + ), + asUint("header") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("header"), ._tag = .header }, + ), + asUint("footer") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("footer"), ._tag = .footer }, + ), + asUint("select") => return createHtmlElementT( + frame, + Element.Html.Select, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("option") => return createHtmlElementT( + frame, + Element.Html.Option, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("iframe") => return createHtmlElementT( + frame, + IFrame, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("figure") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("figure"), ._tag = .figure }, + ), + asUint("hgroup") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("hgroup"), ._tag = .hgroup }, + ), + else => {}, + }, + 7 => switch (@as(u56, @bitCast(name[0..7].*))) { + asUint("section") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("section"), ._tag = .section }, + ), + asUint("article") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("article"), ._tag = .article }, + ), + asUint("details") => return createHtmlElementT( + frame, + Element.Html.Details, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("summary") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("summary"), ._tag = .summary }, + ), + asUint("caption") => return createHtmlElementT( + frame, + Element.Html.TableCaption, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("marquee") => return createHtmlElementT( + frame, + Element.Html.Marquee, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("address") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("address"), ._tag = .address }, + ), + asUint("picture") => return createHtmlElementT( + frame, + Element.Html.Picture, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + else => {}, + }, + 8 => switch (@as(u64, @bitCast(name[0..8].*))) { + asUint("textarea") => return createHtmlElementT( + frame, + Element.Html.TextArea, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("template") => return createHtmlElementT( + frame, + Element.Html.Template, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._content = undefined }, + ), + asUint("colgroup") => return createHtmlElementT( + frame, + Element.Html.TableCol, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("colgroup"), ._tag = .colgroup }, + ), + asUint("fieldset") => return createHtmlElementT( + frame, + Element.Html.FieldSet, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("frameset") => { + if (comptime from_parser) { + log.warn(.not_implemented, "framset", .{ .note = "... in html is not handled properly" }); + } + return createHtmlElementT( + frame, + Element.Html.FrameSet, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ); + }, + asUint("optgroup") => return createHtmlElementT( + frame, + Element.Html.OptGroup, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("progress") => return createHtmlElementT( + frame, + Element.Html.Progress, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("datalist") => return createHtmlElementT( + frame, + Element.Html.DataList, + namespace, + attribute_iterator, + .{ ._proto = undefined }, + ), + asUint("noscript") => return createHtmlElementT( + frame, + Element.Html.Generic, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("noscript"), ._tag = .noscript }, + ), + else => {}, + }, + 10 => switch (@as(u80, @bitCast(name[0..10].*))) { + asUint("blockquote") => return createHtmlElementT( + frame, + Element.Html.Quote, + namespace, + attribute_iterator, + .{ ._proto = undefined, ._tag_name = comptime .wrap("blockquote"), ._tag = .blockquote }, + ), + else => {}, + }, + else => {}, + } + const tag_name = try String.init(frame.arena, name, .{}); + + // Check if this is a custom element (must have hyphen for HTML namespace) + const has_hyphen = std.mem.indexOfScalar(u8, name, '-') != null; + if (has_hyphen and namespace == .html) { + const definition = frame.window._custom_elements._definitions.get(name); + const node = try createHtmlElementT(frame, Element.Html.Custom, namespace, attribute_iterator, .{ + ._proto = undefined, + ._tag_name = tag_name, + ._definition = definition, + }); + + const def = definition orelse { + const element = node.as(Element); + const custom = element.is(Element.Html.Custom).?; + try frame._undefined_custom_elements.append(frame.arena, custom); + return node; + }; + + // Save and restore upgrading element to allow nested createElement calls + const prev_upgrading = frame._upgrading_element; + frame._upgrading_element = node; + defer frame._upgrading_element = prev_upgrading; + + var ls: JS.Local.Scope = undefined; + frame.js.localScope(&ls); + defer ls.deinit(); + + if (from_parser) { + // There are some things custom elements aren't allowed to do + // when we're parsing. + frame.document._throw_on_dynamic_markup_insertion_counter += 1; + } + defer if (from_parser) { + frame.document._throw_on_dynamic_markup_insertion_counter -= 1; + }; + + var caught: JS.TryCatch.Caught = undefined; + _ = ls.toLocal(def.constructor).newInstance(&caught) catch |err| { + log.warn(.js, "custom element constructor", .{ .name = name, .err = err, .caught = caught, .type = frame._type, .url = frame.url }); + return node; + }; + + // After constructor runs, invoke attributeChangedCallback for initial attributes + const element = node.as(Element); + if (element._attributes) |attributes| { + var it = attributes.iterator(); + while (it.next()) |attr| { + Element.Html.Custom.enqueueAttributeChangedCallbackOnElement( + element, + attr._name, + null, // old_value is null for initial attributes + attr._value, + null, + frame, + ); + } + } + + return node; + } + + return createHtmlElementT(frame, Element.Html.Unknown, namespace, attribute_iterator, .{ ._proto = undefined, ._tag_name = tag_name }); + }, + .svg => { + const tag_name = try String.init(frame.arena, name, .{}); + if (std.ascii.eqlIgnoreCase(name, "svg")) { + return createSvgElementT(frame, Element.Svg, name, attribute_iterator, .{ + ._proto = undefined, + ._type = .svg, + ._tag_name = tag_name, + }); + } + + // Other SVG elements (rect, circle, text, g, etc.) + const lower = std.ascii.lowerString(&frame.buf, name); + const tag = std.meta.stringToEnum(Element.Tag, lower) orelse .unknown; + return createSvgElementT(frame, Element.Svg.Generic, name, attribute_iterator, .{ ._proto = undefined, ._tag = tag }); + }, + else => { + const tag_name = try String.init(frame.arena, name, .{}); + return createHtmlElementT(frame, Element.Html.Unknown, namespace, attribute_iterator, .{ ._proto = undefined, ._tag_name = tag_name }); + }, + } +} + +fn createHtmlElementT(frame: *Frame, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype, html_element: E) !*Node { + const html_element_ptr = try frame._factory.htmlElement(html_element); + const element = html_element_ptr.asElement(); + element._namespace = namespace; + try populateElementAttributes(frame, element, attribute_iterator); + + // Check for customized built-in element via "is" attribute + try Element.Html.Custom.checkAndAttachBuiltIn(element, frame); + + const node = element.asNode(); + if (@hasDecl(E, "Build") and @hasDecl(E.Build, "created")) { + @call(.auto, @field(E.Build, "created"), .{ node, frame }) catch |err| { + log.err(.frame, "build.created", .{ .tag = node.getNodeName(&frame.buf), .err = err, .type = frame._type, .url = frame.url }); + return err; + }; + } + return node; +} + +fn createHtmlMediaElementT(frame: *Frame, comptime E: type, namespace: Element.Namespace, attribute_iterator: anytype) !*Node { + const media_element = try frame._factory.htmlMediaElement(E{ ._proto = undefined }); + const element = media_element.asElement(); + element._namespace = namespace; + try populateElementAttributes(frame, element, attribute_iterator); + return element.asNode(); +} + +fn createSvgElementT(frame: *Frame, comptime E: type, tag_name: []const u8, attribute_iterator: anytype, svg_element: E) !*Node { + const svg_element_ptr = try frame._factory.svgElement(tag_name, svg_element); + var element = svg_element_ptr.asElement(); + element._namespace = .svg; + try populateElementAttributes(frame, element, attribute_iterator); + return element.asNode(); +} + +fn populateElementAttributes(frame: *Frame, element: *Element, list: anytype) !void { + if (@TypeOf(list) == ?*Element.Attribute.List) { + // from cloneNode + + var existing = list orelse return; + + var attributes = try frame.arena.create(Element.Attribute.List); + attributes.* = .{ + .normalize = existing.normalize, + }; + + var it = existing.iterator(); + while (it.next()) |attr| { + try attributes.putNew(attr._name.str(), attr._value.str(), frame); + } + element._attributes = attributes; + return; + } + + // from the parser + if (@TypeOf(list) == @TypeOf(null) or list.count() == 0) { + return; + } + var attributes = try element.createAttributeList(frame); + while (list.next()) |attr| { + try attributes.putNew(attr.name.local.slice(), attr.value.slice(), frame); + } +} + +// Called when `new MyElement()` is invoked directly in JS (not via the +// customElements.define/upgrade path). `new_target` is the constructor +// function that was used with `new`. We find the matching definition in the +// registry by function identity and allocate a detached Custom element with +// the registered tag name. +pub fn constructCustomElement(frame: *Frame, new_target: JS.Function) !*Element { + var it = frame.window._custom_elements._definitions.iterator(); + const definition = while (it.next()) |entry| { + if (entry.value_ptr.*.constructor.isEqual(new_target)) { + break entry.value_ptr.*; + } + } else return error.IllegalConstructor; + + // Customized built-ins (`class Foo extends HTMLDivElement`, etc.) would + // need to allocate the extended HTML type rather than Custom. Not yet + // supported via direct `new` — upgrade path still works for those. + if (definition.isCustomizedBuiltIn()) { + return error.IllegalConstructor; + } + + const tag_name = try String.init(frame.arena, definition.name, .{}); + const node = try createHtmlElementT(frame, Element.Html.Custom, .html, @as(?*Element.Attribute.List, null), .{ + ._proto = undefined, + ._tag_name = tag_name, + ._definition = definition, + }); + return node.as(Element); +} + +pub fn createTextNode(frame: *Frame, text: []const u8) !*Node { + const cd = try frame._factory.node(CData{ + ._proto = undefined, + ._type = .{ .text = .{ + ._proto = undefined, + } }, + ._data = try frame.dupeSSO(text), + }); + cd._type.text._proto = cd; + return cd.asNode(); +} + +pub fn createComment(frame: *Frame, text: []const u8) !*Node { + const cd = try frame._factory.node(CData{ + ._proto = undefined, + ._type = .{ .comment = .{ + ._proto = undefined, + } }, + ._data = try frame.dupeSSO(text), + }); + cd._type.comment._proto = cd; + return cd.asNode(); +} + +pub fn createCDATASection(frame: *Frame, data: []const u8) !*Node { + // Validate that the data doesn't contain "]]>" + if (std.mem.indexOf(u8, data, "]]>") != null) { + return error.InvalidCharacterError; + } + + // First allocate the Text node separately + const text_node = try frame._factory.create(CData.Text{ + ._proto = undefined, + }); + + // Then create the CData with cdata_section variant + const cd = try frame._factory.node(CData{ + ._proto = undefined, + ._type = .{ .cdata_section = .{ + ._proto = text_node, + } }, + ._data = try frame.dupeSSO(data), + }); + + // Set up the back pointer from Text to CData + text_node._proto = cd; + + return cd.asNode(); +} + +pub fn createProcessingInstruction(frame: *Frame, target: []const u8, data: []const u8) !*Node { + // Validate neither target nor data contain "?>" + if (std.mem.indexOf(u8, target, "?>") != null) { + return error.InvalidCharacterError; + } + if (std.mem.indexOf(u8, data, "?>") != null) { + return error.InvalidCharacterError; + } + + // Validate target follows XML Name production + try validateXmlName(target); + + const owned_target = try frame.dupeString(target); + + const pi = try frame._factory.create(CData.ProcessingInstruction{ + ._proto = undefined, + ._target = owned_target, + }); + + const cd = try frame._factory.node(CData{ + ._proto = undefined, + ._type = .{ .processing_instruction = pi }, + ._data = try frame.dupeSSO(data), + }); + + // Set up the back pointer from ProcessingInstruction to CData + pi._proto = cd; + + return cd.asNode(); +} + +/// Validate a string against the XML Name production. +/// https://www.w3.org/TR/xml/#NT-Name +fn validateXmlName(name: []const u8) !void { + if (name.len == 0) return error.InvalidCharacterError; + + var i: usize = 0; + + // First character must be a NameStartChar. + const first_len = std.unicode.utf8ByteSequenceLength(name[0]) catch + return error.InvalidCharacterError; + if (first_len > name.len) return error.InvalidCharacterError; + const first_cp = std.unicode.utf8Decode(name[0..][0..first_len]) catch + return error.InvalidCharacterError; + if (!isXmlNameStartChar(first_cp)) return error.InvalidCharacterError; + i = first_len; + + // Subsequent characters must be NameChars. + while (i < name.len) { + const cp_len = std.unicode.utf8ByteSequenceLength(name[i]) catch + return error.InvalidCharacterError; + if (i + cp_len > name.len) return error.InvalidCharacterError; + const cp = std.unicode.utf8Decode(name[i..][0..cp_len]) catch + return error.InvalidCharacterError; + if (!isXmlNameChar(cp)) return error.InvalidCharacterError; + i += cp_len; + } +} + +fn isXmlNameStartChar(c: u21) bool { + return c == ':' or + (c >= 'A' and c <= 'Z') or + c == '_' or + (c >= 'a' and c <= 'z') or + (c >= 0xC0 and c <= 0xD6) or + (c >= 0xD8 and c <= 0xF6) or + (c >= 0xF8 and c <= 0x2FF) or + (c >= 0x370 and c <= 0x37D) or + (c >= 0x37F and c <= 0x1FFF) or + (c >= 0x200C and c <= 0x200D) or + (c >= 0x2070 and c <= 0x218F) or + (c >= 0x2C00 and c <= 0x2FEF) or + (c >= 0x3001 and c <= 0xD7FF) or + (c >= 0xF900 and c <= 0xFDCF) or + (c >= 0xFDF0 and c <= 0xFFFD) or + (c >= 0x10000 and c <= 0xEFFFF); +} + +fn isXmlNameChar(c: u21) bool { + return isXmlNameStartChar(c) or + c == '-' or + c == '.' or + (c >= '0' and c <= '9') or + c == 0xB7 or + (c >= 0x300 and c <= 0x36F) or + (c >= 0x203F and c <= 0x2040); +} + +fn asUint(comptime string: anytype) std.meta.Int( + .unsigned, + @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0 +) { + const byteLength = @sizeOf(@TypeOf(string.*)) - 1; + const expectedType = *const [byteLength:0]u8; + if (@TypeOf(string) != expectedType) { + @compileError("expected : " ++ @typeName(expectedType) ++ ", got: " ++ @typeName(@TypeOf(string))); + } + + return @bitCast(@as(*const [byteLength]u8, string).*); +} diff --git a/src/browser/frame/observers.zig b/src/browser/frame/observers.zig new file mode 100644 index 000000000..e909308df --- /dev/null +++ b/src/browser/frame/observers.zig @@ -0,0 +1,258 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Per-frame MutationObserver and IntersectionObserver bookkeeping: registration, +// the scheduling of the microtask deliveries, and broadcasting DOM mutations to +// the registered observers. The state lives on the Frame (frame._mutation / +// frame._intersection); these functions operate on it. + +const std = @import("std"); +const lp = @import("lightpanda"); + +const Frame = @import("../Frame.zig"); +const Page = @import("../Page.zig"); + +const Node = @import("../webapi/Node.zig"); +const Event = @import("../webapi/Event.zig"); +const Element = @import("../webapi/Element.zig"); +const MutationObserver = @import("../webapi/MutationObserver.zig"); +const IntersectionObserver = @import("../webapi/IntersectionObserver.zig"); + +const log = lp.log; +const String = lp.String; + +// MutationObserver bookkeeping for a frame. +pub const Mutation = struct { + // List of active MutationObservers + observers: std.DoublyLinkedList = .{}, + delivery_scheduled: bool = false, + delivery_depth: u32 = 0, +}; + +// IntersectionObserver bookkeeping for a frame. +pub const Intersection = struct { + // List of active IntersectionObservers + observers: std.ArrayList(*IntersectionObserver) = .{}, + check_scheduled: bool = false, + delivery_scheduled: bool = false, +}; + +// Releases the frame's references to its registered observers. Called from +// Frame.deinit. +pub fn deinit(frame: *Frame, page: *Page) void { + var node: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first; + while (node) |n| { + node = n.next; // capture before we potentially delete observer + const observer: *MutationObserver = @fieldParentPtr("node", n); + observer.releaseRef(page); + } + + for (frame._intersection.observers.items) |observer| { + observer.releaseRef(page); + } +} + +pub fn registerMutationObserver(frame: *Frame, observer: *MutationObserver) !void { + observer.acquireRef(); + frame._mutation.observers.append(&observer.node); +} + +pub fn unregisterMutationObserver(frame: *Frame, observer: *MutationObserver) void { + observer.releaseRef(frame._page); + frame._mutation.observers.remove(&observer.node); +} + +pub fn registerIntersectionObserver(frame: *Frame, observer: *IntersectionObserver) !void { + observer.acquireRef(); + try frame._intersection.observers.append(frame.arena, observer); +} + +pub fn unregisterIntersectionObserver(frame: *Frame, observer: *IntersectionObserver) void { + for (frame._intersection.observers.items, 0..) |obs, i| { + if (obs == observer) { + observer.releaseRef(frame._page); + _ = frame._intersection.observers.swapRemove(i); + return; + } + } +} + +pub fn hasMutationObservers(frame: *const Frame) bool { + return frame._mutation.observers.first != null; +} + +pub fn checkIntersections(frame: *Frame) !void { + for (frame._intersection.observers.items) |observer| { + try observer.checkIntersections(frame); + } +} + +pub fn scheduleMutationDelivery(frame: *Frame) !void { + if (frame._mutation.delivery_scheduled) { + return; + } + frame._mutation.delivery_scheduled = true; + try frame.js.queueMutationDelivery(); +} + +pub fn scheduleIntersectionDelivery(frame: *Frame) !void { + if (frame._intersection.delivery_scheduled) { + return; + } + frame._intersection.delivery_scheduled = true; + try frame.js.queueIntersectionDelivery(); +} + +pub fn performScheduledIntersectionChecks(frame: *Frame) void { + if (!frame._intersection.check_scheduled) { + return; + } + frame._intersection.check_scheduled = false; + checkIntersections(frame) catch |err| { + log.err(.frame, "frame.schedIntersectChecks", .{ .err = err, .type = frame._type, .url = frame.url }); + }; +} + +pub fn deliverIntersections(frame: *Frame) void { + if (!frame._intersection.delivery_scheduled) { + return; + } + frame._intersection.delivery_scheduled = false; + + // Iterate backwards to handle observers that disconnect during their callback + var i = frame._intersection.observers.items.len; + while (i > 0) { + i -= 1; + const observer = frame._intersection.observers.items[i]; + observer.deliverEntries(frame) catch |err| { + log.err(.frame, "frame.deliverIntersections", .{ .err = err, .type = frame._type, .url = frame.url }); + }; + } +} + +pub fn deliverMutations(frame: *Frame) void { + if (!frame._mutation.delivery_scheduled) { + return; + } + frame._mutation.delivery_scheduled = false; + + frame._mutation.delivery_depth += 1; + defer if (!frame._mutation.delivery_scheduled) { + // reset the depth once nothing is left to be scheduled + frame._mutation.delivery_depth = 0; + }; + + if (frame._mutation.delivery_depth > 100) { + log.err(.frame, "frame.MutationLimit", .{ .type = frame._type, .url = frame.url }); + frame._mutation.delivery_depth = 0; + return; + } + + // snapshot the pending slots to deliver. We'll deliver these AFTER the mutation + // but new pending slots that land during mutation should only be delivered + // on the microtask tick. + const slots = frame.call_arena.dupe(*Element.Html.Slot, frame._slots_pending_slotchange.keys()) catch |err| blk: { + log.err(.frame, "deliverMutations.slots", .{ .err = err, .type = frame._type, .url = frame.url }); + break :blk &.{}; + }; + frame._slots_pending_slotchange.clearRetainingCapacity(); + + // We only deliver notifications for observers that have records BEFORE + // we started the delivery. So we need to snapshot this. Any observers which + // get records during this phase will only be processed on the next microtask tick. + var notify: std.ArrayList(*MutationObserver) = .empty; + var it: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first; + while (it) |node| : (it = node.next) { + const observer: *MutationObserver = @fieldParentPtr("node", node); + if (observer._pending_records.items.len == 0) { + continue; + } + notify.append(frame.call_arena, observer) catch |err| { + log.err(.frame, "deliverMutations.notify", .{ .err = err, .type = frame._type, .url = frame.url }); + break; + }; + } + + for (notify.items) |observer| { + observer.deliverRecords(frame) catch |err| { + log.err(.frame, "frame.deliverMutations", .{ .err = err, .type = frame._type, .url = frame.url }); + }; + } + + // slotchange events fire after the observer callbacks (spec step order) + for (slots) |slot| { + const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, frame._page) catch |err| { + log.err(.frame, "deliverSlotchange.init", .{ .err = err, .type = frame._type, .url = frame.url }); + continue; + }; + const target = slot.asNode().asEventTarget(); + frame._event_manager.dispatch(target, event) catch |err| { + log.err(.frame, "deliverSlotchange.dispatch", .{ .err = err, .type = frame._type, .url = frame.url }); + }; + } +} + +// Broadcast an attribute change to every registered MutationObserver. The +// caller (Frame.attributeChange / attributeRemove) handles the non-observer +// side effects (build hooks, custom-element callbacks, slot/popover updates). +pub fn notifyAttributeChange(frame: *Frame, element: *Element, name: String, old_value: ?String) void { + var it: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first; + while (it) |node| : (it = node.next) { + const observer: *MutationObserver = @fieldParentPtr("node", node); + observer.notifyAttributeChange(element, name, old_value, frame) catch |err| { + log.err(.frame, "attributeChange.notifyObserver", .{ .err = err, .type = frame._type, .url = frame.url }); + }; + } +} + +pub fn notifyCharacterDataChange(frame: *Frame, target: *Node, old_value: String) void { + var it: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first; + while (it) |node| : (it = node.next) { + const observer: *MutationObserver = @fieldParentPtr("node", node); + observer.notifyCharacterDataChange(target, old_value, frame) catch |err| { + log.err(.frame, "cdataChange.notifyObserver", .{ .err = err, .type = frame._type, .url = frame.url }); + }; + } +} + +pub fn notifyChildListChange( + frame: *Frame, + target: *Node, + added_nodes: []const *Node, + removed_nodes: []const *Node, + previous_sibling: ?*Node, + next_sibling: ?*Node, +) void { + // Filter out HTML wrapper element during fragment parsing (html5ever quirk) + if (frame._parse_mode == .fragment and added_nodes.len == 1) { + if (added_nodes[0].is(Element.Html.Html) != null) { + // This is the temporary HTML wrapper, added by html5ever + // that will be unwrapped, see: + // https://github.com/servo/html5ever/issues/583 + return; + } + } + + var it: ?*std.DoublyLinkedList.Node = frame._mutation.observers.first; + while (it) |node| : (it = node.next) { + const observer: *MutationObserver = @fieldParentPtr("node", node); + observer.notifyChildListChange(target, added_nodes, removed_nodes, previous_sibling, next_sibling, frame) catch |err| { + log.err(.frame, "childListChange.notifyObserver", .{ .err = err, .type = frame._type, .url = frame.url }); + }; + } +} diff --git a/src/browser/frame/user_input.zig b/src/browser/frame/user_input.zig new file mode 100644 index 000000000..a9c761a8f --- /dev/null +++ b/src/browser/frame/user_input.zig @@ -0,0 +1,477 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Synthetic user input driving the DOM: mouse, wheel, keyboard, focus +// navigation and text insertion. These are mostly fed by CDP's Input domain +// (src/cdp/domains/input.zig) and by EventManager's default activation +// behavior. Form submission itself lives on the Frame (it's a navigation +// concern); the activation paths here call into it. + +const std = @import("std"); +const lp = @import("lightpanda"); +const builtin = @import("builtin"); + +const Frame = @import("../Frame.zig"); + +const Node = @import("../webapi/Node.zig"); +const Event = @import("../webapi/Event.zig"); +const Element = @import("../webapi/Element.zig"); +const TreeWalker = @import("../webapi/TreeWalker.zig"); +const MouseEvent = @import("../webapi/event/MouseEvent.zig"); +const WheelEvent = @import("../webapi/event/WheelEvent.zig"); +const KeyboardEvent = @import("../webapi/event/KeyboardEvent.zig"); + +const log = lp.log; +const IS_DEBUG = builtin.mode == .Debug; + +// DOM MouseEvent.button values. +// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button +pub const mouse_button = struct { + pub const main: i32 = 0; // left + pub const auxiliary: i32 = 1; // middle + pub const secondary: i32 = 2; // right + pub const fourth: i32 = 3; // back + pub const fifth: i32 = 4; // forward +}; + +// Dispatch a single trusted mouse event of the given type on `target`, carrying +// the pressed button and pointer position. `detail` is the click count (used for +// click/dblclick); 0 for events where it does not apply. +fn dispatchMouseEventOn(frame: *Frame, target: *Element, comptime typ: []const u8, x: f64, y: f64, button: i32, detail: u32) !void { + const event: *MouseEvent = try .initTrusted(comptime .wrap(typ), .{ + .bubbles = true, + .cancelable = true, + .composed = true, + .clientX = x, + .clientY = y, + .button = button, + .detail = detail, + }, frame); + try frame._event_manager.dispatch(target.asEventTarget(), event.asEvent()); +} + +pub fn triggerMousePress(frame: *Frame, x: f64, y: f64, button: i32) !void { + const target = (try frame.window._document.elementFromPoint(x, y, frame)) orelse return; + if (comptime IS_DEBUG) { + log.debug(.frame, "frame mouse press", .{ + .url = frame.url, + .node = target, + .x = x, + .y = y, + .button = button, + .type = frame._type, + }); + } + try dispatchMouseEventOn(frame, target, "mousedown", x, y, button, 0); +} + +pub fn triggerMouseMove(frame: *Frame, x: f64, y: f64) !void { + const target = (try frame.window._document.elementFromPoint(x, y, frame)) orelse return; + if (comptime IS_DEBUG) { + log.debug(.frame, "frame mouse move", .{ + .url = frame.url, + .node = target, + .x = x, + .y = y, + .type = frame._type, + }); + } + + const move_event: *MouseEvent = try .initTrusted(comptime .wrap("mousemove"), .{ + .bubbles = true, + .cancelable = true, + .composed = true, + .clientX = x, + .clientY = y, + }, frame); + try frame._event_manager.dispatch(target.asEventTarget(), move_event.asEvent()); + + const over_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseover"), .{ + .bubbles = true, + .cancelable = true, + .composed = true, + .clientX = x, + .clientY = y, + }, frame); + try frame._event_manager.dispatch(target.asEventTarget(), over_event.asEvent()); + + const enter_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseenter"), .{ + .composed = true, + .clientX = x, + .clientY = y, + }, frame); + try frame._event_manager.dispatch(target.asEventTarget(), enter_event.asEvent()); +} + +pub fn triggerMouseRelease(frame: *Frame, x: f64, y: f64, button: i32, click_count: i32) !void { + const target = (try frame.window._document.elementFromPoint(x, y, frame)) orelse return; + if (comptime IS_DEBUG) { + log.debug(.frame, "frame mouse release", .{ + .url = frame.url, + .node = target, + .x = x, + .y = y, + .button = button, + .type = frame._type, + }); + } + + const detail: u32 = if (click_count > 0) @intCast(click_count) else 1; + + try dispatchMouseEventOn(frame, target, "mouseup", x, y, button, detail); + + // After mouseup, the activation event depends on the button. + switch (button) { + mouse_button.main => { + try dispatchMouseEventOn(frame, target, "click", x, y, button, detail); + // A second click in quick succession also fires dblclick. + if (click_count == 2) { + try dispatchMouseEventOn(frame, target, "dblclick", x, y, button, detail); + } + }, + mouse_button.auxiliary => try dispatchMouseEventOn(frame, target, "auxclick", x, y, button, detail), + mouse_button.secondary => try dispatchMouseEventOn(frame, target, "contextmenu", x, y, button, detail), + else => {}, + } +} + +pub fn triggerMouseWheel(frame: *Frame, x: f64, y: f64, delta_x: f64, delta_y: f64) !void { + const target = (try frame.window._document.elementFromPoint(x, y, frame)) orelse return; + if (comptime IS_DEBUG) { + log.debug(.frame, "frame mouse wheel", .{ + .url = frame.url, + .node = target, + .x = x, + .y = y, + .delta_x = delta_x, + .delta_y = delta_y, + .type = frame._type, + }); + } + + const wheel_event: *WheelEvent = try .initTrusted("wheel", .{ + .bubbles = true, + .cancelable = true, + .composed = true, + .clientX = x, + .clientY = y, + .deltaX = delta_x, + .deltaY = delta_y, + }, frame); + + // Keep the event alive past dispatch so we can read _prevent_default. + wheel_event.asEvent().acquireRef(); + defer _ = wheel_event.asEvent().releaseRef(frame._page); + try frame._event_manager.dispatch(target.asEventTarget(), wheel_event.asEvent()); + + if (wheel_event.asEvent()._prevent_default) { + return; + } + + // Apply the scroll and fire a trusted scroll event, mirroring WebDriver wheel. + // CDP deltas are untrusted, so guard NaN and saturate the addition. + const new_left: i32 = @as(i32, @intCast(target.getScrollLeft(frame))) +| deltaToScroll(delta_x); + const new_top: i32 = @as(i32, @intCast(target.getScrollTop(frame))) +| deltaToScroll(delta_y); + try target.setScrollLeft(new_left, frame); + try target.setScrollTop(new_top, frame); + + const scroll_event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, frame._page); + try frame._event_manager.dispatch(target.asEventTarget(), scroll_event); +} + +fn deltaToScroll(d: f64) i32 { + if (std.math.isNan(d)) return 0; + return @intFromFloat(std.math.clamp(d, std.math.minInt(i32), std.math.maxInt(i32))); +} + +// callback when the "click" event reaches the frame. +pub fn handleClick(frame: *Frame, target: *Node) !void { + // TODO: Also support elements when implement + const element = target.is(Element) orelse return; + const html_element = element.is(Element.Html) orelse return; + + switch (html_element._type) { + .anchor => |anchor| { + const href = element.getAttributeSafe(comptime .wrap("href")) orelse return; + if (href.len == 0) { + return; + } + + if (std.mem.startsWith(u8, href, "javascript:")) { + return; + } + + if (try element.hasAttribute(comptime .wrap("download"), frame)) { + log.warn(.browser, "a.download", .{ .type = frame._type, .url = frame.url }); + return; + } + + const target_frame = blk: { + const target_name = anchor.getTarget(); + if (target_name.len == 0) { + break :blk target.ownerFrame(frame); + } + break :blk frame.resolveTargetFrame(target_name) orelse { + log.warn(.not_implemented, "target", .{ .type = frame._type, .url = frame.url, .target = target_name }); + return; + }; + }; + + try element.focus(frame); + try frame.scheduleNavigation(href, .{ + .reason = .script, + .kind = .{ .push = null }, + }, .{ .anchor = target_frame }); + }, + .input => |input| { + try element.focus(frame); + // Per HTML §4.10.18.6.4 "Image Button state (type=image)", clicking an + // image button submits its form. The form-data set already gets the + // submitter's coordinate fields appended via FormData.collectForm + // (see src/browser/webapi/net/FormData.zig). + if (input._input_type == .submit or input._input_type == .image) { + return frame.submitForm(element, input.getForm(frame), .{}); + } + }, + .button => |button| { + try element.focus(frame); + if (std.mem.eql(u8, button.getType(), "submit")) { + return frame.submitForm(element, button.getForm(frame), .{}); + } + }, + .select, .textarea => try element.focus(frame), + .label => |label| { + // Per HTML §4.10.4 "The label element", a label's activation + // behavior is to run the synthetic click activation steps on the + // labeled control. Mirrors Chrome's HTMLLabelElement::DefaultEventHandler. + const control = label.getControl(frame) orelse return; + const control_html = control.is(Element.Html) orelse return; + try control_html.click(frame); + }, + .generic => |generic| { + switch (generic._tag) { + .summary => { + const parent_el = target.parentElement() orelse return; + const details = parent_el.is(Element.Html.Details) orelse return; + var maybe_prev = element.previousElementSibling(); + while (maybe_prev) |prev| { + if (prev.getTag() == .summary) { + // we found a summary element before the clicked one + return; + } + maybe_prev = prev.previousElementSibling(); + } + try details.setOpen(!details.getOpen(), frame); + }, + else => {}, + } + }, + else => {}, + } +} + +pub fn triggerKeyboard(frame: *Frame, keyboard_event: *KeyboardEvent) !void { + const event = keyboard_event.asEvent(); + // Dispatch to the effective active element. When nothing is explicitly + // focused this resolves to (matching `document.activeElement`), so + // the keydown still fires and its default action — e.g. sequential focus + // navigation on Tab — can run. + const element = frame.window._document.getActiveElement() orelse { + event.deinit(frame._page); + return; + }; + + if (comptime IS_DEBUG) { + log.debug(.frame, "frame keydown", .{ + .url = frame.url, + .node = element, + .key = keyboard_event._key, + .type = frame._type, + }); + } + try frame._event_manager.dispatch(element.asEventTarget(), event); +} + +pub fn handleKeydown(frame: *Frame, target: *Node, event: *Event) !void { + const keyboard_event = event.is(KeyboardEvent) orelse return; + const key = keyboard_event.getKey(); + + if (key == .Dead) { + return; + } + + if (key == .Tab) { + // tab -> forward, shift+tab -> backwards + return moveFocus(frame, keyboard_event.getShiftKey() == false); + } + + if (target.is(Element.Html.Input)) |input| { + if (key == .Enter) { + return frame.submitForm(input.asElement(), input.getForm(frame), .{}); + } + + // Don't handle text input for radio/checkbox + const input_type = input._input_type; + if (input_type == .radio or input_type == .checkbox) { + return; + } + + // Handle printable characters + if (key.isPrintable()) { + try input.innerInsert(key.asString(), frame); + } + return; + } + + if (target.is(Element.Html.TextArea)) |textarea| { + // zig fmt: off + const append = + if (key == .Enter) "\n" + else if (key.isPrintable()) key.asString() + else return + ; + // zig fmt: on + return textarea.innerInsert(append, frame); + } +} + +// Sequential focus navigation: move `document.activeElement` to the next (Tab) +// or previous (Shift+Tab) focusable element, firing the usual blur/focus events +// via `Element.focus`. The order is fully determined by tabindex + document +// position, so no layout is needed: +// 1. elements with a positive tabindex, in ascending tabindex order; +// 2. then elements with tabindex 0 (or a natively-focusable default), in +// document order. +// Ties within a group break on document order, and Tab wraps around at the ends. +// https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation +fn moveFocus(frame: *Frame, forward: bool) !void { + const document = frame.document; + const current = document._active_element; + + const current_tab_index = blk: { + const cur = current orelse break :blk 0; + const current_html = cur.is(Element.Html) orelse break :blk 0; + break :blk current_html.getTabIndex(); + }; + + // Single document-order pass tracking two candidates: + // edge — the global first (forward) / last (backward) focusable element, + // used to wrap around when `current` is at an end, or as the + // landing spot when nothing is focused yet. + // chosen — the closest focusable element strictly past `current` in the + // travel direction. + var edge: ?*Element = null; + var edge_tab_index: i32 = 0; + + var chosen: ?*Element = null; + var chosen_tab_index: i32 = 0; + + var tw = TreeWalker.Full.Elements.init(document.asNode(), .{}); + while (tw.next()) |candidate| { + if (candidate.isDisabled()) { + continue; + } + if (candidate.is(Element.Html) == null) { + continue; + } + + const candidate_tab_index = blk: { + if (candidate.getAttributeSafe(comptime .wrap("tabindex"))) |attr| { + if (Element.Html.parseInteger(attr)) |tab_index| { + if (tab_index < 0) { + continue; + } + break :blk tab_index; + } + break :blk 0; + } + + // no tab index, maybe this item isn't focusable.. + const focusable = switch (candidate.getTag()) { + .button, .select, .textarea, .iframe => true, + .input => candidate.as(Element.Html.Input)._input_type != .hidden, + .anchor, .area => candidate.getAttributeSafe(comptime .wrap("href")) != null, + else => false, + }; + if (focusable == false) { + continue; + } + + break :blk 0; + }; + + if (edge == null or focusOrderBefore(candidate, candidate_tab_index, edge.?, edge_tab_index) == forward) { + edge = candidate; + edge_tab_index = candidate_tab_index; + } + + const cur = current orelse continue; + + if (candidate == cur) { + continue; + } + + const past = if (forward) focusOrderBefore(cur, current_tab_index, candidate, candidate_tab_index) else focusOrderBefore(candidate, candidate_tab_index, cur, current_tab_index); + if (!past) { + continue; + } + if (chosen == null or focusOrderBefore(candidate, candidate_tab_index, chosen.?, chosen_tab_index) == forward) { + chosen = candidate; + chosen_tab_index = candidate_tab_index; + } + } + + const next = chosen orelse edge orelse return; + try next.focus(frame); +} + +// Orders two focusable elements by sequential focus navigation order: positive +// tabindex first (ascending), then tabindex 0, ties broken by document order. +fn focusOrderBefore(a: *Element, a_tab_index: i32, b: *Element, b_tab_index: i32) bool { + if (a_tab_index == b_tab_index) { + // Equal tabindex → document order: `a` precedes `b` when `b` follows `a`. + const FOLLOWING: u16 = 0x04; + return (a.asNode().compareDocumentPosition(b.asNode()) & FOLLOWING) != 0; + } + + const group_a: u8 = if (a_tab_index > 0) 0 else 1; + const group_b: u8 = if (b_tab_index > 0) 0 else 1; + if (group_a != group_b) { + return group_a < group_b; + } + + return a_tab_index < b_tab_index; +} + +// insertText is a shortcut to insert text into the active element. +pub fn insertText(frame: *Frame, v: []const u8) !void { + const html_element = frame.document._active_element orelse return; + + if (html_element.is(Element.Html.Input)) |input| { + const input_type = input._input_type; + if (input_type == .radio or input_type == .checkbox) { + return; + } + + return input.innerInsert(v, frame); + } + + if (html_element.is(Element.Html.TextArea)) |textarea| { + return textarea.innerInsert(v, frame); + } +} diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index ea121e64d..089841e22 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -1039,7 +1039,7 @@ pub fn queueMutationDelivery(self: *Context) !void { self.enqueueMicrotask(struct { fn run(ctx: *Context) void { switch (ctx.global) { - .frame => |frame| frame.deliverMutations(), + .frame => |frame| Frame.observers.deliverMutations(frame), .worker => unreachable, } } @@ -1050,7 +1050,7 @@ pub fn queueIntersectionChecks(self: *Context) !void { self.enqueueMicrotask(struct { fn run(ctx: *Context) void { switch (ctx.global) { - .frame => |frame| frame.performScheduledIntersectionChecks(), + .frame => |frame| Frame.observers.performScheduledIntersectionChecks(frame), .worker => unreachable, } } @@ -1061,7 +1061,7 @@ pub fn queueIntersectionDelivery(self: *Context) !void { self.enqueueMicrotask(struct { fn run(ctx: *Context) void { switch (ctx.global) { - .frame => |frame| frame.deliverIntersections(), + .frame => |frame| Frame.observers.deliverIntersections(frame), .worker => unreachable, } } diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index be02eb8e5..35611bf98 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -142,7 +142,7 @@ fn appendTextChunk(self: *Parser, parent: *Node, txt: []const u8) !void { // Fresh text run: the first chunk lives on _data only. buf stays empty // until (and unless) a second chunk arrives. - const new_text = try self.frame.createTextNode(txt); + const new_text = try Frame.node_factory.createTextNode(self.frame, txt); try self.frame.appendNew(parent, new_text); self.pending_text = .{ .parent = parent, @@ -446,7 +446,7 @@ fn _createElementCallback(self: *Parser, data: *anyopaque, qname: h5e.QualName, const name = qname.local.slice(); const namespace_string = qname.ns.slice(); const namespace = if (namespace_string.len == 0) default_namespace else Element.Namespace.parse(namespace_string); - const node = try frame.createElementNS(namespace, name, attributes); + const node = try Frame.node_factory.createElementNS(frame, namespace, name, attributes); const pn = try self.arena.create(ParsedNode); pn.* = .{ @@ -467,7 +467,7 @@ fn createCommentCallback(ctx: *anyopaque, str: h5e.StringSlice) callconv(.c) ?*a } fn _createCommentCallback(self: *Parser, str: []const u8) !*anyopaque { const frame = self.frame; - const node = try frame.createComment(str); + const node = try Frame.node_factory.createComment(frame, str); const pn = try self.arena.create(ParsedNode); pn.* = .{ .data = null, @@ -487,7 +487,7 @@ fn createProcessingInstruction(ctx: *anyopaque, target: h5e.StringSlice, data: h } fn _createProcessingInstruction(self: *Parser, target: []const u8, data: []const u8) !*anyopaque { const frame = self.frame; - const node = try frame.createProcessingInstruction(target, data); + const node = try Frame.node_factory.createProcessingInstruction(frame, target, data); const pn = try self.arena.create(ParsedNode); pn.* = .{ .data = null, @@ -697,7 +697,7 @@ fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e } break :blk child; }, - .text => |txt| try self.frame.createTextNode(txt), + .text => |txt| try Frame.node_factory.createTextNode(self.frame, txt), }; try self.frame.insertNodeRelative(parent, node, .{ .before = sibling }, .{}); } diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index f3627802a..eaa44120c 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -235,7 +235,7 @@ pub fn setData(self: *CData, value: ?[]const u8, frame: *Frame) !void { self._data = .empty; } - frame.characterDataChange(self.asNode(), old_value); + Frame.observers.notifyCharacterDataChange(frame, self.asNode(), old_value); } /// JS bridge wrapper for `data` setter. @@ -305,7 +305,7 @@ pub fn deleteData(self: *CData, offset: usize, count: usize, frame: *Frame) !voi old_value[range.end..], }); } - frame.characterDataChange(self.asNode(), old_data); + Frame.observers.notifyCharacterDataChange(frame, self.asNode(), old_data); } pub fn insertData(self: *CData, offset: usize, data: []const u8, frame: *Frame) !void { @@ -321,7 +321,7 @@ pub fn insertData(self: *CData, offset: usize, data: []const u8, frame: *Frame) data, existing[byte_offset..], }); - frame.characterDataChange(self.asNode(), old_value); + Frame.observers.notifyCharacterDataChange(frame, self.asNode(), old_value); } pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, frame: *Frame) !void { @@ -340,7 +340,7 @@ pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, data, existing[range.end..], }); - frame.characterDataChange(self.asNode(), old_value); + Frame.observers.notifyCharacterDataChange(frame, self.asNode(), old_value); } pub fn substringData(self: *const CData, offset: usize, count: usize) ![]const u8 { diff --git a/src/browser/webapi/DOMImplementation.zig b/src/browser/webapi/DOMImplementation.zig index c8197dc68..23eac9c98 100644 --- a/src/browser/webapi/DOMImplementation.zig +++ b/src/browser/webapi/DOMImplementation.zig @@ -44,20 +44,20 @@ pub fn createHTMLDocument(_: *const DOMImplementation, title: ?js.NullableString _ = try document.asNode().appendChild(doctype.asNode(), frame); } - const html_node = try frame.createElementNS(.html, "html", null); + const html_node = try Frame.node_factory.createElementNS(frame, .html, "html", null); _ = try document.asNode().appendChild(html_node, frame); - const head_node = try frame.createElementNS(.html, "head", null); + const head_node = try Frame.node_factory.createElementNS(frame, .html, "head", null); _ = try html_node.appendChild(head_node, frame); if (title) |t| { - const title_node = try frame.createElementNS(.html, "title", null); + const title_node = try Frame.node_factory.createElementNS(frame, .html, "title", null); _ = try head_node.appendChild(title_node, frame); - const text_node = try frame.createTextNode(t.value); + const text_node = try Frame.node_factory.createTextNode(frame, t.value); _ = try title_node.appendChild(text_node, frame); } - const body_node = try frame.createElementNS(.html, "body", null); + const body_node = try Frame.node_factory.createElementNS(frame, .html, "body", null); _ = try html_node.appendChild(body_node, frame); return document; @@ -77,7 +77,7 @@ pub fn createDocument(_: *const DOMImplementation, namespace_: ?[]const u8, qual if (qualified_name) |qname| { if (qname.len > 0) { const namespace = Node.Element.Namespace.parse(namespace_); - const root = try frame.createElementNS(namespace, qname, null); + const root = try Frame.node_factory.createElementNS(frame, namespace, qname, null); _ = try document.asNode().appendChild(root, frame); } } diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 7373e825b..07cb80f6c 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -165,7 +165,7 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement }; // HTML documents are case-insensitive - lowercase the tag name - const node = try frame.createElementNS(ns, normalized_name, null); + const node = try Frame.node_factory.createElementNS(frame, ns, normalized_name, null); const element = node.as(Element); // Track owner document if it's not the main document @@ -186,7 +186,7 @@ pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8 try validateElementName(name); const ns = Element.Namespace.parse(namespace); // Per spec, createElementNS does NOT lowercase (unlike createElement). - const node = try frame.createElementNS(ns, name, null); + const node = try Frame.node_factory.createElementNS(frame, ns, name, null); // Store original URI for unknown namespaces so lookupNamespaceURI can return it if (ns == .unknown) { @@ -317,7 +317,7 @@ pub fn createDocumentFragment(self: *Document, frame: *Frame) !*Node.DocumentFra } pub fn createComment(self: *Document, data: []const u8, frame: *Frame) !*Node { - const node = try frame.createComment(data); + const node = try Frame.node_factory.createComment(frame, data); // Track owner document if it's not the main document if (self != frame.document) { try frame.setNodeOwnerDocument(node, self); @@ -326,7 +326,7 @@ pub fn createComment(self: *Document, data: []const u8, frame: *Frame) !*Node { } pub fn createTextNode(self: *Document, data: []const u8, frame: *Frame) !*Node { - const node = try frame.createTextNode(data); + const node = try Frame.node_factory.createTextNode(frame, data); // Track owner document if it's not the main document if (self != frame.document) { try frame.setNodeOwnerDocument(node, self); @@ -337,8 +337,8 @@ pub fn createTextNode(self: *Document, data: []const u8, frame: *Frame) !*Node { pub fn createCDATASection(self: *Document, data: []const u8, frame: *Frame) !*Node { const node = switch (self._type) { .html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument - .xml => try frame.createCDATASection(data), - .generic => try frame.createCDATASection(data), + .xml => try Frame.node_factory.createCDATASection(frame, data), + .generic => try Frame.node_factory.createCDATASection(frame, data), }; // Track owner document if it's not the main document if (self != frame.document) { @@ -348,7 +348,7 @@ pub fn createCDATASection(self: *Document, data: []const u8, frame: *Frame) !*No } pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, frame: *Frame) !*Node { - const node = try frame.createProcessingInstruction(target, data); + const node = try Frame.node_factory.createProcessingInstruction(frame, target, data); // Track owner document if it's not the main document if (self != frame.document) { try frame.setNodeOwnerDocument(node, self); @@ -1129,9 +1129,9 @@ fn _injectBlank(self: *Document, frame: *Frame) !void { std.debug.assert(self.asNode()._children == null); } - const html = try frame.createElementNS(.html, "html", null); - const head = try frame.createElementNS(.html, "head", null); - const body = try frame.createElementNS(.html, "body", null); + const html = try Frame.node_factory.createElementNS(frame, .html, "html", null); + const head = try Frame.node_factory.createElementNS(frame, .html, "head", null); + const body = try Frame.node_factory.createElementNS(frame, .html, "body", null); try frame.appendNode(html, head, .{}); try frame.appendNode(html, body, .{}); try frame.appendNode(self.asNode(), html, .{}); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 9674e444d..b8d47fcdc 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -797,7 +797,7 @@ pub fn insertAdjacentText( data: []const u8, frame: *Frame, ) !void { - const text_node = try frame.createTextNode(data); + const text_node = try Frame.node_factory.createTextNode(frame, data); const target_node, const prev_node = try self.asNode().findAdjacentNodes(where); _ = try target_node.insertBefore(text_node, prev_node, frame); } @@ -1494,7 +1494,7 @@ pub fn getElementsByClassName(self: *Element, class_name: []const u8, frame: *Fr pub fn clone(self: *Element, deep: bool, frame: *Frame) !*Node { const tag_name = self.getTagNameDump(); - const node = try frame.createElementNS(self._namespace, tag_name, self._attributes); + const node = try Frame.node_factory.createElementNS(frame, self._namespace, tag_name, self._attributes); // Allow element-specific types to copy their runtime state _ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), deep, frame }) catch |err| { diff --git a/src/browser/webapi/HTMLDocument.zig b/src/browser/webapi/HTMLDocument.zig index 44e0ccfae..ba4d39438 100644 --- a/src/browser/webapi/HTMLDocument.zig +++ b/src/browser/webapi/HTMLDocument.zig @@ -66,7 +66,7 @@ pub fn setBody(self: *HTMLDocument, html: []const u8, frame: *Frame) !void { // Build a fresh holding the parsed HTML as its children. Fragment // parsing strips any // wrappers the author included. - const new_body_node = try frame.createElementNS(.html, "body", null); + const new_body_node = try Frame.node_factory.createElementNS(frame, .html, "body", null); if (html.len > 0) { try frame.parseHtmlAsChildren(new_body_node, html); } @@ -153,7 +153,7 @@ pub fn setTitle(self: *HTMLDocument, title: []const u8, frame: *Frame) !void { } // No title element found, create one - const title_node = try frame.createElementNS(.html, "title", null); + const title_node = try Frame.node_factory.createElementNS(frame, .html, "title", null); const title_element = title_node.as(Element); // Only add text if non-empty diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index da7f288b9..8d9d98e4d 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -139,7 +139,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, frame: *Frame) !vo try self._observing.append(self._arena, target); if (self._observing.items.len == 1) { - try frame.registerIntersectionObserver(self); + try Frame.observers.registerIntersectionObserver(frame, self); } try self._tracked.put(self._arena, target, {}); @@ -147,7 +147,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, frame: *Frame) !vo // Check intersection for this new target and schedule delivery try self.checkIntersection(target, frame); if (self._pending_entries.items.len > 0) { - try frame.scheduleIntersectionDelivery(); + try Frame.observers.scheduleIntersectionDelivery(frame); } } @@ -174,7 +174,7 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, frame: *Frame) v } if (original_length > 0 and self._observing.items.len == 0) { - frame.unregisterIntersectionObserver(self); + Frame.observers.unregisterIntersectionObserver(frame, self); } } @@ -186,7 +186,7 @@ pub fn disconnect(self: *IntersectionObserver, frame: *Frame) void { self._tracked.clearRetainingCapacity(); if (self._observing.items.len > 0) { - frame.unregisterIntersectionObserver(self); + Frame.observers.unregisterIntersectionObserver(frame, self); } self._observing.clearRetainingCapacity(); } @@ -298,7 +298,7 @@ pub fn checkIntersections(self: *IntersectionObserver, frame: *Frame) !void { } if (self._pending_entries.items.len > 0) { - try frame.scheduleIntersectionDelivery(); + try Frame.observers.scheduleIntersectionDelivery(frame); } } diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig index c7b401906..5b696cc97 100644 --- a/src/browser/webapi/MutationObserver.zig +++ b/src/browser/webapi/MutationObserver.zig @@ -171,7 +171,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, }); if (self._observing.items.len == 1) { - try frame.registerMutationObserver(self); + try Frame.observers.registerMutationObserver(frame, self); } } @@ -182,7 +182,7 @@ pub fn disconnect(self: *MutationObserver, frame: *Frame) void { self._pending_records.clearRetainingCapacity(); if (self._observing.items.len > 0) { - frame.unregisterMutationObserver(self); + Frame.observers.unregisterMutationObserver(frame, self); } self._observing.clearRetainingCapacity(); } @@ -244,7 +244,7 @@ pub fn notifyAttributeChange( try self._pending_records.append(self._arena, record); - try frame.scheduleMutationDelivery(); + try Frame.observers.scheduleMutationDelivery(frame); break; } } @@ -288,7 +288,7 @@ pub fn notifyCharacterDataChange( try self._pending_records.append(self._arena, record); - try frame.scheduleMutationDelivery(); + try Frame.observers.scheduleMutationDelivery(frame); break; } } @@ -332,7 +332,7 @@ pub fn notifyChildListChange( try self._pending_records.append(self._arena, record); - try frame.scheduleMutationDelivery(); + try Frame.observers.scheduleMutationDelivery(frame); break; } } diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 907c00291..b61bcda0c 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -622,12 +622,12 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, frame: *Fra if (new_node == ref_node_) { frame.domChanged(); - if (frame.hasMutationObservers()) { + if (Frame.observers.hasMutationObservers(frame)) { const parent = new_node._parent.?; const previous_sibling = new_node.previousSibling(); const next_sibling = new_node.nextSibling(); const replaced = [_]*Node{new_node}; - frame.childListChange(parent, &replaced, &replaced, previous_sibling, next_sibling); + Frame.observers.notifyChildListChange(frame, parent, &replaced, &replaced, previous_sibling, next_sibling); } return new_node; @@ -832,10 +832,10 @@ pub fn cloneNode(self: *Node, deep_: ?bool, frame: *Frame) CloneError!*Node { .cdata => |cd| { const data = cd.getData().str(); return switch (cd._type) { - .text => frame.createTextNode(data), - .cdata_section => frame.createCDATASection(data), - .comment => frame.createComment(data), - .processing_instruction => |pi| frame.createProcessingInstruction(pi._target, data), + .text => Frame.node_factory.createTextNode(frame, data), + .cdata_section => Frame.node_factory.createCDATASection(frame, data), + .comment => Frame.node_factory.createComment(frame, data), + .processing_instruction => |pi| Frame.node_factory.createProcessingInstruction(frame, pi._target, data), }; }, .element => |el| return el.clone(deep, frame), @@ -1314,7 +1314,7 @@ pub const NodeOrText = union(enum) { pub fn toNode(self: *const NodeOrText, frame: *Frame) !*Node { return switch (self.*) { .node => |n| n, - .text => |txt| frame.createTextNode(txt), + .text => |txt| Frame.node_factory.createTextNode(frame, txt), }; } diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index b2a464ca1..a617125e3 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -353,8 +353,8 @@ pub fn insertNode(self: *Range, node: *Node, frame: *Frame) !void { const before_text = text_data[0..offset]; const after_text = text_data[offset..]; - const before = try frame.createTextNode(before_text); - const after = try frame.createTextNode(after_text); + const before = try Frame.node_factory.createTextNode(frame, before_text); + const after = try Frame.node_factory.createTextNode(frame, after_text); _ = try parent.replaceChild(before, container, frame); _ = try parent.insertBefore(node, before.nextSibling(), frame); @@ -390,7 +390,7 @@ pub fn deleteContents(self: *Range, frame: *Frame) !void { frame.arena, &.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] }, ); - frame.characterDataChange(self._proto._start_container, old_value); + Frame.observers.notifyCharacterDataChange(frame, self._proto._start_container, old_value); } else { // Delete child nodes in range. // Capture count before the loop: removeChild triggers live range @@ -459,7 +459,7 @@ pub fn cloneContents(self: *const Range, frame: *Frame) !*DocumentFragment { const text_data = self._proto._start_container.getData().str(); if (self._proto._start_offset < text_data.len and self._proto._end_offset <= text_data.len) { const cloned_text = text_data[self._proto._start_offset..self._proto._end_offset]; - const text_node = try frame.createTextNode(cloned_text); + const text_node = try Frame.node_factory.createTextNode(frame, cloned_text); _ = try fragment.asNode().appendChild(text_node, frame); } } else { @@ -481,7 +481,7 @@ pub fn cloneContents(self: *const Range, frame: *Frame) !*DocumentFragment { if (self._proto._start_offset < text_data.len) { // Clone from start_offset to end of text const cloned_text = text_data[self._proto._start_offset..]; - const text_node = try frame.createTextNode(cloned_text); + const text_node = try Frame.node_factory.createTextNode(frame, cloned_text); _ = try fragment.asNode().appendChild(text_node, frame); } } @@ -504,7 +504,7 @@ pub fn cloneContents(self: *const Range, frame: *Frame) !*DocumentFragment { if (self._proto._end_offset > 0 and self._proto._end_offset <= text_data.len) { // Clone from start to end_offset const cloned_text = text_data[0..self._proto._end_offset]; - const text_node = try frame.createTextNode(cloned_text); + const text_node = try Frame.node_factory.createTextNode(frame, cloned_text); _ = try fragment.asNode().appendChild(text_node, frame); } } @@ -550,9 +550,9 @@ pub fn createContextualFragment(self: *const Range, html: []const u8, frame: *Fr // Create a temporary element of the same type as the context for parsing // This preserves the parsing context without modifying the original node const temp_node = if (context_node.is(Node.Element)) |el| - try frame.createElementNS(el._namespace, el.getTagNameLower(), null) + try Frame.node_factory.createElementNS(frame, el._namespace, el.getTagNameLower(), null) else - try frame.createElementNS(.html, "div", null); + try Frame.node_factory.createElementNS(frame, .html, "div", null); try frame.parseContextualFragment(temp_node, html); diff --git a/src/browser/webapi/cdata/Comment.zig b/src/browser/webapi/cdata/Comment.zig index 6d77dd356..3c9f7db3b 100644 --- a/src/browser/webapi/cdata/Comment.zig +++ b/src/browser/webapi/cdata/Comment.zig @@ -26,7 +26,7 @@ const Comment = @This(); _proto: *CData, pub fn init(str: ?js.NullableString, frame: *Frame) !*Comment { - const node = try frame.createComment(if (str) |s| s.value else ""); + const node = try Frame.node_factory.createComment(frame, if (str) |s| s.value else ""); return node.as(Comment); } diff --git a/src/browser/webapi/cdata/Text.zig b/src/browser/webapi/cdata/Text.zig index 977a4489d..e358813b7 100644 --- a/src/browser/webapi/cdata/Text.zig +++ b/src/browser/webapi/cdata/Text.zig @@ -27,7 +27,7 @@ const Text = @This(); _proto: *CData, pub fn init(str: ?js.NullableString, frame: *Frame) !*Text { - const node = try frame.createTextNode(if (str) |s| s.value else ""); + const node = try Frame.node_factory.createTextNode(frame, if (str) |s| s.value else ""); return node.as(Text); } @@ -45,7 +45,7 @@ pub fn splitText(self: *Text, offset: usize, frame: *Frame) !*Text { const byte_offset = CData.utf16OffsetToUtf8(data, offset) catch return error.IndexSizeError; const new_data = data[byte_offset..]; - const new_node = try frame.createTextNode(new_data); + const new_node = try Frame.node_factory.createTextNode(frame, new_data); const new_text = new_node.as(Text); const node = self._proto.asNode(); diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 871a0882d..610c7afcd 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -116,7 +116,7 @@ pub fn construct(new_target: js.Function, frame: *Frame) !*Element { if (frame._upgrading_element) |node| { return node.is(Element) orelse return error.IllegalConstructor; } - return frame.constructCustomElement(new_target); + return Frame.node_factory.constructCustomElement(frame, new_target); } pub const Type = union(enum) { @@ -288,7 +288,7 @@ pub fn setInnerText(self: *HtmlElement, text: []const u8, frame: *Frame) !void { } // Create and append text node - const text_node = try frame.createTextNode(text); + const text_node = try Frame.node_factory.createTextNode(frame, text); try frame.appendNode(parent, text_node, .{ .child_already_connected = false }); } diff --git a/src/browser/webapi/element/html/Audio.zig b/src/browser/webapi/element/html/Audio.zig index 81b994409..8d6663396 100644 --- a/src/browser/webapi/element/html/Audio.zig +++ b/src/browser/webapi/element/html/Audio.zig @@ -32,7 +32,7 @@ const Audio = @This(); _proto: *Media, pub fn constructor(maybe_url: ?String, frame: *Frame) !*Media { - const node = try frame.createElementNS(.html, "audio", null); + const node = try Frame.node_factory.createElementNS(frame, .html, "audio", null); const el = node.as(Element); const list = try el.getOrCreateAttributeList(frame); diff --git a/src/browser/webapi/element/html/Image.zig b/src/browser/webapi/element/html/Image.zig index 47835309e..e1b4cf56f 100644 --- a/src/browser/webapi/element/html/Image.zig +++ b/src/browser/webapi/element/html/Image.zig @@ -9,7 +9,7 @@ const Image = @This(); _proto: *HtmlElement, pub fn constructor(w_: ?u32, h_: ?u32, frame: *Frame) !*Image { - const node = try frame.createElementNS(.html, "img", null); + const node = try Frame.node_factory.createElementNS(frame, .html, "img", null); const el = node.as(Element); if (w_) |w| blk: { diff --git a/src/browser/webapi/element/html/TextArea.zig b/src/browser/webapi/element/html/TextArea.zig index db43bf927..da9698faf 100644 --- a/src/browser/webapi/element/html/TextArea.zig +++ b/src/browser/webapi/element/html/TextArea.zig @@ -106,7 +106,7 @@ pub fn setDefaultValue(self: *TextArea, value: []const u8, frame: *Frame) !void } // No text child exists, create one - const text_node = try frame.createTextNode(value); + const text_node = try Frame.node_factory.createTextNode(frame, value); _ = try node.appendChild(text_node, frame); } diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig index a865ee344..33fc0756d 100644 --- a/src/cdp/domains/input.zig +++ b/src/cdp/domains/input.zig @@ -18,8 +18,9 @@ const std = @import("std"); const CDP = @import("../CDP.zig"); +const Frame = @import("../../browser/Frame.zig"); -const dom_button = @import("../../browser/Frame.zig").mouse_button; +const dom_button = Frame.user_input.mouse_button; pub fn processMessage(cmd: *CDP.Command) !void { const action = std.meta.stringToEnum(enum { @@ -74,7 +75,7 @@ fn dispatchKeyEvent(cmd: *CDP.Command) !void { .metaKey = params.modifiers & 4 == 4, .shiftKey = params.modifiers & 8 == 8, }, frame); - try frame.triggerKeyboard(keyboard_event); + try Frame.user_input.triggerKeyboard(frame, keyboard_event); // result already sent } @@ -124,10 +125,10 @@ fn dispatchMouseEvent(cmd: *CDP.Command) !void { }; switch (params.type) { - .mousePressed => try frame.triggerMousePress(params.x, params.y, button), - .mouseReleased => try frame.triggerMouseRelease(params.x, params.y, button, params.clickCount), - .mouseMoved => try frame.triggerMouseMove(params.x, params.y), - .mouseWheel => try frame.triggerMouseWheel(params.x, params.y, params.deltaX, params.deltaY), + .mousePressed => try Frame.user_input.triggerMousePress(frame, params.x, params.y, button), + .mouseReleased => try Frame.user_input.triggerMouseRelease(frame, params.x, params.y, button, params.clickCount), + .mouseMoved => try Frame.user_input.triggerMouseMove(frame, params.x, params.y), + .mouseWheel => try Frame.user_input.triggerMouseWheel(frame, params.x, params.y, params.deltaX, params.deltaY), } // result already sent } @@ -141,7 +142,7 @@ fn insertText(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return; const frame = bc.session.currentFrame() orelse return; - try frame.insertText(params.text); + try Frame.user_input.insertText(frame, params.text); try cmd.sendResult(null, .{}); }