From c9882e10a49de77b58e50789244dd9913d6648e1 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 4 Dec 2025 14:39:15 +0800 Subject: [PATCH] Properly handle insertion of DocumentFragment Add various CData methods XHR and Fetch request headers Animation mocks --- src/browser/Page.zig | 18 + src/browser/js/Object.zig | 6 +- src/browser/js/bridge.zig | 1 + src/browser/tests/cdata/character_data.html | 730 ++++++++++++++++++ .../tests/document_fragment/insertion.html | 238 ++++++ src/browser/tests/net/request.html | 6 + src/browser/webapi/CData.zig | 133 ++++ src/browser/webapi/DOMException.zig | 4 + src/browser/webapi/Element.zig | 21 +- src/browser/webapi/KeyValueList.zig | 31 + src/browser/webapi/Node.zig | 6 +- src/browser/webapi/Window.zig | 2 +- src/browser/webapi/animation/Animation.zig | 49 ++ src/browser/webapi/net/Fetch.zig | 10 +- src/browser/webapi/net/Headers.zig | 31 +- src/browser/webapi/net/Request.zig | 14 +- src/browser/webapi/net/Response.zig | 2 +- src/browser/webapi/net/URLSearchParams.zig | 21 +- src/browser/webapi/net/XMLHttpRequest.zig | 13 +- 19 files changed, 1288 insertions(+), 48 deletions(-) create mode 100644 src/browser/tests/cdata/character_data.html create mode 100644 src/browser/tests/document_fragment/insertion.html create mode 100644 src/browser/webapi/animation/Animation.zig 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 +// 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 . + +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); + +const Animation = @This(); + +pub fn init() !Animation { + return .{}; +} + +pub fn play(_: *Animation) void {} +pub fn pause(_: *Animation) void {} +pub fn cancel(_: *Animation) void {} +pub fn finish(_: *Animation) void {} +pub fn reverse(_: *Animation) void {} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Animation); + + pub const Meta = struct { + pub const name = "Animation"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; + }; + + pub const play = bridge.function(Animation.play, .{}); + pub const pause = bridge.function(Animation.pause, .{}); + pub const cancel = bridge.function(Animation.cancel, .{}); + pub const finish = bridge.function(Animation.finish, .{}); + pub const reverse = bridge.function(Animation.reverse, .{}); +}; diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 0f6c37f3..50d44270 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -40,10 +40,10 @@ _response: *Response, _resolver: js.PersistentPromiseResolver, pub const Input = Request.Input; -pub const RequestInit = Request.Options; +pub const InitOpts = Request.InitOpts; // @ZIGDOM just enough to get campfire demo working -pub fn init(input: Input, options: ?RequestInit, page: *Page) !js.Promise { +pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise { const request = try Request.init(input, options, page); const fetch = try page.arena.create(Fetch); @@ -56,7 +56,11 @@ pub fn init(input: Input, options: ?RequestInit, page: *Page) !js.Promise { }; const http_client = page._session.browser.http_client; - const headers = try http_client.newHeaders(); + var headers = try http_client.newHeaders(); + if (request._headers) |h| { + try h.populateHttpHeader(page.call_arena, &headers); + } + try page.requestCookie(.{}).headersForRequest(page.arena, request._url, &headers); if (comptime IS_DEBUG) { log.debug(.http, "fetch", .{ .url = request._url }); diff --git a/src/browser/webapi/net/Headers.zig b/src/browser/webapi/net/Headers.zig index 136207bd..63377179 100644 --- a/src/browser/webapi/net/Headers.zig +++ b/src/browser/webapi/net/Headers.zig @@ -5,16 +5,34 @@ const log = @import("../../../log.zig"); const Page = @import("../../Page.zig"); const KeyValueList = @import("../KeyValueList.zig"); +const Allocator = std.mem.Allocator; + const Headers = @This(); _list: KeyValueList, -pub fn init(page: *Page) !*Headers { +pub const InitOpts = union(enum) { + obj: *Headers, + js_obj: js.Object, +}; + +pub fn init(opts_: ?InitOpts, page: *Page) !*Headers { + const list = if (opts_) |opts| switch (opts) { + .obj => |obj| try KeyValueList.copy(page.arena, obj._list), + .js_obj => |js_obj| try KeyValueList.fromJsObject(page.arena, js_obj), + } else KeyValueList.init(); + return page._factory.create(Headers{ - ._list = KeyValueList.init(), + ._list = list, }); } +// pub fn fromJsObject(js_obj: js.Object, page: *Page) !*Headers { +// return page._factory.create(Headers{ +// ._list = try KeyValueList.fromJsObject(page.arena, js_obj), +// }); +// } + pub fn append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void { const normalized_name = normalizeHeaderName(name, page); try self._list.append(page.arena, normalized_name, value); @@ -63,6 +81,15 @@ pub fn forEach(self: *Headers, cb_: js.Function, js_this_: ?js.Object) !void { } } +// TODO: do we really need 2 different header structs?? +const Http = @import("../../../http/Http.zig"); +pub fn populateHttpHeader(self: *Headers, allocator: Allocator, http_headers: *Http.Headers) !void { + for (self._list._entries.items) |entry| { + const merged = try std.mem.concatWithSentinel(allocator, u8, &.{ entry.name.str(), ": ", entry.value.str() }, 0); + try http_headers.add(merged); + } +} + fn normalizeHeaderName(name: []const u8, page: *Page) []const u8 { if (name.len > page.buf.len) { return name; diff --git a/src/browser/webapi/net/Request.zig b/src/browser/webapi/net/Request.zig index d1524afe..9ca84c41 100644 --- a/src/browser/webapi/net/Request.zig +++ b/src/browser/webapi/net/Request.zig @@ -37,19 +37,19 @@ pub const Input = union(enum) { url: [:0]const u8, }; -pub const Options = struct { +pub const InitOpts = struct { method: ?[]const u8 = null, - headers: ?*Headers = null, + headers: ?Headers.InitOpts = null, }; -pub fn init(input: Input, opts_: ?Options, page: *Page) !*Request { +pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request { const arena = page.arena; const url = switch (input) { .url => |u| try URL.resolve(arena, page.url, u, .{ .always_dupe = true }), .request => |r| try arena.dupeZ(u8, r._url), }; - const opts = opts_ orelse Options{}; + const opts = opts_ orelse InitOpts{}; const method = if (opts.method) |m| try parseMethod(m, page) else switch (input) { @@ -57,8 +57,8 @@ pub fn init(input: Input, opts_: ?Options, page: *Page) !*Request { .request => |r| r._method, }; - const headers = if (opts.headers) |h| - h + const headers = if (opts.headers) |header_init| + try Headers.init(header_init, page) else switch (input) { .url => null, .request => |r| r._headers, @@ -103,7 +103,7 @@ pub fn getHeaders(self: *Request, page: *Page) !*Headers { return headers; } - const headers = try Headers.init(page); + const headers = try Headers.init(null, page); self._headers = headers; return headers; } diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index 24447566..fe464320 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -56,7 +56,7 @@ pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response { ._arena = page.arena, ._status = opts.status, ._body = body, - ._headers = opts.headers orelse try Headers.init(page), + ._headers = opts.headers orelse try Headers.init(null, page), ._type = .basic, // @ZIGDOM: todo }); } diff --git a/src/browser/webapi/net/URLSearchParams.zig b/src/browser/webapi/net/URLSearchParams.zig index 9bdecd2e..73e5e110 100644 --- a/src/browser/webapi/net/URLSearchParams.zig +++ b/src/browser/webapi/net/URLSearchParams.zig @@ -45,7 +45,7 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams { .query_string => |qs| break :blk try paramsFromString(arena, qs, &page.buf), .value => |js_val| { if (js_val.isObject()) { - break :blk try paramsFromObject(arena, js_val.toObject()); + break :blk try KeyValueList.fromJsObject(arena, js_val.toObject()); } if (js_val.isString()) { break :blk try paramsFromString(arena, try js_val.toString(arena), &page.buf); @@ -187,25 +187,6 @@ fn paramsFromString(allocator: Allocator, input_: []const u8, buf: []u8) !KeyVal return params; } -fn paramsFromObject(arena: Allocator, js_obj: js.Object) !KeyValueList { - var it = js_obj.nameIterator(arena); - - var params = KeyValueList.init(); - try params.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 params._entries.append(arena, .{ - .name = try String.init(arena, name, .{}), - .value = try String.init(arena, value, .{}), - }); - } - - return params; -} - fn unescape(arena: Allocator, value: []const u8, buf: []u8) !String { if (value.len == 0) { return String.init(undefined, "", .{}); diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 4b0cdb9f..4959d563 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -26,6 +26,7 @@ const URL = @import("../../URL.zig"); const Mime = @import("../../Mime.zig"); const Page = @import("../../Page.zig"); const Event = @import("../Event.zig"); +const Headers = @import("Headers.zig"); const EventTarget = @import("../EventTarget.zig"); const XMLHttpRequestEventTarget = @import("XMLHttpRequestEventTarget.zig"); @@ -40,6 +41,7 @@ _transfer: ?*Http.Transfer = null, _url: [:0]const u8 = "", _method: Http.Method = .GET, +_request_headers: *Headers, _request_body: ?[]const u8 = null, _response: std.ArrayList(u8) = .empty, @@ -71,6 +73,7 @@ pub fn init(page: *Page) !*XMLHttpRequest { ._page = page, ._proto = undefined, ._arena = page.arena, + ._request_headers = try Headers.init(null, page), }); } @@ -129,6 +132,10 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void try self.stateChanged(.opened, self._page); } +pub fn setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8, page: *Page) !void { + return self._request_headers.append(name, value, page); +} + pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void { if (comptime IS_DEBUG) { log.debug(.http, "XMLHttpRequest.send", .{ .url = self._url }); @@ -143,10 +150,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void { const page = self._page; const http_client = page._session.browser.http_client; var headers = try http_client.newHeaders(); - // @ZIGDOM - // for (self._headers.items) |hdr| { - // try headers.add(hdr); - // } + try self._request_headers.populateHttpHeader(page.call_arena, &headers); try page.requestCookie(.{}).headersForRequest(self._arena, self._url, &headers); try http_client.request(.{ @@ -351,6 +355,7 @@ pub const JsApi = struct { pub const responseType = bridge.accessor(XMLHttpRequest.getResponseType, XMLHttpRequest.setResponseType, .{}); pub const status = bridge.accessor(XMLHttpRequest.getStatus, null, .{}); pub const response = bridge.accessor(XMLHttpRequest.getResponse, null, .{}); + pub const setRequestHeader = bridge.function(XMLHttpRequest.setRequestHeader, .{}); }; const testing = @import("../../../testing.zig");