diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 7f8ad2bc..6c94e453 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -1431,6 +1431,24 @@ pub fn appendAllChildren(self: *Page, parent: *Node, target: *Node) !void { } } +pub fn insertAllChildrenBefore(self: *Page, fragment: *Node, target: *Node, ref_node: *Node) !void { + self.domChanged(); + const dest_connected = target.isConnected(); + + var it = fragment.childrenIterator(); + while (it.next()) |child| { + // Check if child was connected BEFORE removing it from fragment + const child_was_connected = child.isConnected(); + self.removeNode(fragment, child, .{ .will_be_reconnected = dest_connected }); + try self.insertNodeRelative( + target, + child, + .{ .before = ref_node }, + .{ .child_already_connected = child_was_connected }, + ); + } +} + fn _appendNode(self: *Page, comptime from_parser: bool, parent: *Node, child: *Node, opts: InsertNodeOpts) !void { self._insertNodeRelative(from_parser, parent, child, .append, opts); } diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig index 222f2b75..9ab35fe1 100644 --- a/src/browser/js/Object.zig +++ b/src/browser/js/Object.zig @@ -135,7 +135,7 @@ pub fn isNullOrUndefined(self: Object) bool { return self.js_obj.toValue().isNullOrUndefined(); } -pub fn nameIterator(self: Object, allocator: Allocator) NameIterator { +pub fn nameIterator(self: Object) NameIterator { const context = self.context; const js_obj = self.js_obj; @@ -145,7 +145,6 @@ pub fn nameIterator(self: Object, allocator: Allocator) NameIterator { return .{ .count = count, .context = context, - .allocator = allocator, .js_obj = array.castTo(v8.Object), }; } @@ -158,7 +157,6 @@ pub const NameIterator = struct { count: u32, idx: u32 = 0, js_obj: v8.Object, - allocator: Allocator, context: *const Context, pub fn next(self: *NameIterator) !?[]const u8 { @@ -170,6 +168,6 @@ pub const NameIterator = struct { const context = self.context; const js_val = try self.js_obj.getAtIndex(context.v8_context, idx); - return try context.valueToString(js_val, .{ .allocator = self.allocator }); + return try context.valueToString(js_val, .{}); } }; diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index e20d9b7f..372d260a 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -568,6 +568,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/media/MediaError.zig"), @import("../webapi/media/TextTrackCue.zig"), @import("../webapi/media/VTTCue.zig"), + @import("../webapi/animation/Animation.zig"), @import("../webapi/EventTarget.zig"), @import("../webapi/Location.zig"), @import("../webapi/Navigator.zig"), diff --git a/src/browser/tests/cdata/character_data.html b/src/browser/tests/cdata/character_data.html new file mode 100644 index 00000000..85513b00 --- /dev/null +++ b/src/browser/tests/cdata/character_data.html @@ -0,0 +1,730 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/document_fragment/insertion.html b/src/browser/tests/document_fragment/insertion.html new file mode 100644 index 00000000..f110766c --- /dev/null +++ b/src/browser/tests/document_fragment/insertion.html @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/tests/net/request.html b/src/browser/tests/net/request.html index c0028cf8..437c2630 100644 --- a/src/browser/tests/net/request.html +++ b/src/browser/tests/net/request.html @@ -45,6 +45,11 @@ const req = new Request('https://example.com/api', { headers }); testing.expectEqual('value', req.headers.get('X-Custom')); } + +{ + const req = new Request('https://example.com/api', {headers: {over: '9000!'}}); + testing.expectEqual('9000!', req.headers.get('over')); +} + diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index cb39a70b..f02d51ca 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -79,6 +79,119 @@ pub fn format(self: *const CData, writer: *std.io.Writer) !void { }; } +pub fn getLength(self: *const CData) usize { + return self._data.len; +} + +pub fn appendData(self: *CData, data: []const u8, page: *Page) !void { + const new_data = try std.mem.concat(page.arena, u8, &.{ self._data, data }); + try self.setData(new_data, page); +} + +pub fn deleteData(self: *CData, offset: usize, count: usize, page: *Page) !void { + if (offset > self._data.len) return error.IndexSizeError; + const end = @min(offset + count, self._data.len); + + // Just slice - original data stays in arena + const old_value = self._data; + if (offset == 0) { + self._data = self._data[end..]; + } else if (end >= self._data.len) { + self._data = self._data[0..offset]; + } else { + self._data = try std.mem.concat(page.arena, u8, &.{ + self._data[0..offset], + self._data[end..], + }); + } + page.characterDataChange(self.asNode(), old_value); +} + +pub fn insertData(self: *CData, offset: usize, data: []const u8, page: *Page) !void { + if (offset > self._data.len) return error.IndexSizeError; + const new_data = try std.mem.concat(page.arena, u8, &.{ + self._data[0..offset], + data, + self._data[offset..], + }); + try self.setData(new_data, page); +} + +pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, page: *Page) !void { + if (offset > self._data.len) return error.IndexSizeError; + const end = @min(offset + count, self._data.len); + const new_data = try std.mem.concat(page.arena, u8, &.{ + self._data[0..offset], + data, + self._data[end..], + }); + try self.setData(new_data, page); +} + +pub fn substringData(self: *const CData, offset: usize, count: usize) ![]const u8 { + if (offset > self._data.len) return error.IndexSizeError; + const end = @min(offset + count, self._data.len); + return self._data[offset..end]; +} + +pub fn remove(self: *CData, page: *Page) !void { + const node = self.asNode(); + const parent = node.parentNode() orelse return; + _ = try parent.removeChild(node, page); +} + +pub fn before(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void { + const node = self.asNode(); + const parent = node.parentNode() orelse return; + + for (nodes) |node_or_text| { + const child = try node_or_text.toNode(page); + _ = try parent.insertBefore(child, node, page); + } +} + +pub fn after(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void { + const node = self.asNode(); + const parent = node.parentNode() orelse return; + const next = node.nextSibling(); + + for (nodes) |node_or_text| { + const child = try node_or_text.toNode(page); + _ = try parent.insertBefore(child, next, page); + } +} + +pub fn replaceWith(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void { + const node = self.asNode(); + const parent = node.parentNode() orelse return; + const next = node.nextSibling(); + + _ = try parent.removeChild(node, page); + + for (nodes) |node_or_text| { + const child = try node_or_text.toNode(page); + _ = try parent.insertBefore(child, next, page); + } +} + +pub fn nextElementSibling(self: *CData) ?*Node.Element { + var maybe_sibling = self.asNode().nextSibling(); + while (maybe_sibling) |sibling| { + if (sibling.is(Node.Element)) |el| return el; + maybe_sibling = sibling.nextSibling(); + } + return null; +} + +pub fn previousElementSibling(self: *CData) ?*Node.Element { + var maybe_sibling = self.asNode().previousSibling(); + while (maybe_sibling) |sibling| { + if (sibling.is(Node.Element)) |el| return el; + maybe_sibling = sibling.previousSibling(); + } + return null; +} + pub const JsApi = struct { pub const bridge = js.Bridge(CData); @@ -89,4 +202,24 @@ pub const JsApi = struct { }; pub const data = bridge.accessor(CData.getData, CData.setData, .{}); + pub const length = bridge.accessor(CData.getLength, null, .{}); + + pub const appendData = bridge.function(CData.appendData, .{}); + pub const deleteData = bridge.function(CData.deleteData, .{ .dom_exception = true }); + pub const insertData = bridge.function(CData.insertData, .{ .dom_exception = true }); + pub const replaceData = bridge.function(CData.replaceData, .{ .dom_exception = true }); + pub const substringData = bridge.function(CData.substringData, .{ .dom_exception = true }); + + pub const remove = bridge.function(CData.remove, .{}); + pub const before = bridge.function(CData.before, .{}); + pub const after = bridge.function(CData.after, .{}); + pub const replaceWith = bridge.function(CData.replaceWith, .{}); + + pub const nextElementSibling = bridge.accessor(CData.nextElementSibling, null, .{}); + pub const previousElementSibling = bridge.accessor(CData.previousElementSibling, null, .{}); }; + +const testing = @import("../../testing.zig"); +test "WebApi: CData" { + try testing.htmlRunner("cdata", .{}); +} diff --git a/src/browser/webapi/DOMException.zig b/src/browser/webapi/DOMException.zig index 2f1cc789..72d79559 100644 --- a/src/browser/webapi/DOMException.zig +++ b/src/browser/webapi/DOMException.zig @@ -33,6 +33,7 @@ pub fn fromError(err: anyerror) ?DOMException { error.NotFound => .{ ._code = .not_found }, error.NotSupported => .{ ._code = .not_supported }, error.HierarchyError => .{ ._code = .hierarchy_error }, + error.IndexSizeError => .{ ._code = .index_size_error }, else => null, }; } @@ -45,6 +46,7 @@ pub fn getName(self: *const DOMException) []const u8 { return switch (self._code) { .none => "Error", .invalid_character_error => "InvalidCharacterError", + .index_size_error => "IndexSizeErorr", .syntax_error => "SyntaxError", .not_found => "NotFoundError", .not_supported => "NotSupportedError", @@ -56,6 +58,7 @@ pub fn getMessage(self: *const DOMException) []const u8 { return switch (self._code) { .none => "", .invalid_character_error => "Invalid Character", + .index_size_error => "IndexSizeError: Index or size is negative or greater than the allowed amount", .syntax_error => "Syntax Error", .not_supported => "Not Supported", .not_found => "Not Found", @@ -65,6 +68,7 @@ pub fn getMessage(self: *const DOMException) []const u8 { const Code = enum(u8) { none = 0, + index_size_error = 1, hierarchy_error = 3, invalid_character_error = 5, not_found = 8, diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 40240953..b3847bc1 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -26,17 +26,18 @@ const Page = @import("../Page.zig"); const reflect = @import("../reflect.zig"); const Node = @import("Node.zig"); +const CSS = @import("CSS.zig"); +const DOMRect = @import("DOMRect.zig"); +const ShadowRoot = @import("ShadowRoot.zig"); const collections = @import("collections.zig"); const Selector = @import("selector/Selector.zig"); -pub const Attribute = @import("element/Attribute.zig"); +const Animation = @import("animation/Animation.zig"); +const DOMStringMap = @import("element/DOMStringMap.zig"); const CSSStyleProperties = @import("css/CSSStyleProperties.zig"); -pub const DOMStringMap = @import("element/DOMStringMap.zig"); -const DOMRect = @import("DOMRect.zig"); -const CSS = @import("CSS.zig"); -const ShadowRoot = @import("ShadowRoot.zig"); pub const Svg = @import("element/Svg.zig"); pub const Html = @import("element/Html.zig"); +pub const Attribute = @import("element/Attribute.zig"); const Element = @This(); @@ -587,6 +588,14 @@ pub fn querySelectorAll(self: *Element, input: []const u8, page: *Page) !*Select return Selector.querySelectorAll(self.asNode(), input, page); } +pub fn getAnimations(_: *const Element) []*Animation { + return &.{}; +} + +pub fn animate(_: *Element, _: js.Object, _: js.Object) !Animation { + return Animation.init(); +} + pub fn closest(self: *Element, selector: []const u8, page: *Page) !?*Element { if (selector.len == 0) { return error.SyntaxError; @@ -1012,6 +1021,8 @@ pub const JsApi = struct { pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true }); pub const closest = bridge.function(Element.closest, .{ .dom_exception = true }); + pub const getAnimations = bridge.function(Element.getAnimations, .{}); + pub const animate = bridge.function(Element.animate, .{}); pub const checkVisibility = bridge.function(Element.checkVisibility, .{}); pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{}); pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{}); diff --git a/src/browser/webapi/KeyValueList.zig b/src/browser/webapi/KeyValueList.zig index c9eb70c8..4ec85f20 100644 --- a/src/browser/webapi/KeyValueList.zig +++ b/src/browser/webapi/KeyValueList.zig @@ -41,9 +41,40 @@ pub const empty: KeyValueList = .{ ._entries = .empty, }; +pub fn copy(arena: Allocator, original: KeyValueList) !KeyValueList { + var list = KeyValueList.init(); + try list.ensureTotalCapacity(arena, original.len()); + for (original._entries.items) |entry| { + try list.appendAssumeCapacity(arena, entry.name.str(), entry.value.str()); + } + return list; +} + +pub fn fromJsObject(arena: Allocator, js_obj: js.Object) !KeyValueList { + var it = js_obj.nameIterator(); + var list = KeyValueList.init(); + try list.ensureTotalCapacity(arena, it.count); + + while (try it.next()) |name| { + const js_value = try js_obj.get(name); + const value = try js_value.toString(arena); + + try list._entries.append(arena, .{ + .name = try String.init(arena, name, .{}), + .value = try String.init(arena, value, .{}), + }); + } + + return list; +} + pub const Entry = struct { name: String, value: String, + + pub fn format(self: Entry, writer: *std.Io.Writer) !void { + return writer.print("{f}: {f}", .{ self.name, self.value }); + } }; pub fn init() KeyValueList { diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index ab0c28ec..8e65a5e9 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -143,7 +143,6 @@ pub fn parentElement(self: *const Node) ?*Element { } pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node { - // Special case: DocumentFragment - append all its children instead if (child.is(DocumentFragment)) |_| { try page.appendAllChildren(child, self); return child; @@ -338,6 +337,11 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page return error.NotFound; } + if (new_node.is(DocumentFragment)) |_| { + try page.insertAllChildrenBefore(new_node, self, ref_node); + return new_node; + } + const child_already_connected = new_node.isConnected(); page.domChanged(); diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index ad755b44..250abb26 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -157,7 +157,7 @@ pub fn setOnUnhandledRejection(self: *Window, cb_: ?js.Function) !void { } } -pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.RequestInit, page: *Page) !js.Promise { +pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.InitOpts, page: *Page) !js.Promise { return Fetch.init(input, options, page); } diff --git a/src/browser/webapi/animation/Animation.zig b/src/browser/webapi/animation/Animation.zig new file mode 100644 index 00000000..2fecfa95 --- /dev/null +++ b/src/browser/webapi/animation/Animation.zig @@ -0,0 +1,49 @@ +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) +// +// Francis Bouvier