diff --git a/src/Server.zig b/src/Server.zig index 481b2cb3..34621efd 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -486,7 +486,7 @@ pub const Client = struct { } // called by CDP - // Websocket frames have a variable lenght header. For server-client, + // Websocket frames have a variable length header. For server-client, // it could be anywhere from 2 to 10 bytes. Our IO.Loop doesn't have // writev, so we need to get creative. We'll JSON serialize to a // buffer, where the first 10 bytes are reserved. We can then backfill diff --git a/src/browser/dump.zig b/src/browser/dump.zig index 03c9bd28..91128732 100644 --- a/src/browser/dump.zig +++ b/src/browser/dump.zig @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) + // Copyright (C) 2023-2025 Lightpanda (Selecy SAS) // // Francis Bouvier // Pierre Tachoire @@ -74,7 +74,11 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri try writer.writeAll(cd.getData()); try writer.writeAll("-->"); } else { - try writeEscapedText(cd.getData(), writer); + if (shouldEscapeText(node._parent)) { + try writeEscapedText(cd.getData(), writer); + } else { + try writer.writeAll(cd.getData()); + } } }, .element => |el| { @@ -245,34 +249,46 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool { return false; } +fn shouldEscapeText(node_: ?*Node) bool { + const node = node_ orelse return true; + if (node.is(Node.Element.Html.Script) != null) { + return false; + } + return true; +} fn writeEscapedText(text: []const u8, writer: *std.Io.Writer) !void { // Fast path: if no special characters, write directly - const first_special = std.mem.indexOfAny(u8, text, "&<>") orelse { + const first_special = std.mem.indexOfAnyPos(u8, text, 0, &.{ '&', '<', '>', 194 }) orelse { return writer.writeAll(text); }; try writer.writeAll(text[0..first_special]); - try writer.writeAll(switch (text[first_special]) { - '&' => "&", - '<' => "<", - '>' => ">", - else => unreachable, - }); + var remaining = try writeEscapedByte(text, first_special, writer); - // Process remaining text - var remaining = text[first_special + 1 ..]; - while (std.mem.indexOfAny(u8, remaining, "&<>")) |offset| { + while (std.mem.indexOfAnyPos(u8, remaining, 0, &.{ '&', '<', '>', 194 })) |offset| { try writer.writeAll(remaining[0..offset]); - try writer.writeAll(switch (remaining[offset]) { - '&' => "&", - '<' => "<", - '>' => ">", - else => unreachable, - }); - remaining = remaining[offset + 1 ..]; + remaining = try writeEscapedByte(remaining, offset, writer); } if (remaining.len > 0) { try writer.writeAll(remaining); } } + +fn writeEscapedByte(input: []const u8, index: usize, writer: *std.Io.Writer) ![]const u8 { + switch (input[index]) { + '&' => try writer.writeAll("&"), + '<' => try writer.writeAll("<"), + '>' => try writer.writeAll(">"), + 194 => { + // non breaking space + if (input.len > index + 1 and input[index + 1] == 160) { + try writer.writeAll(" "); + return input [index + 2 ..]; + } + try writer.writeByte(194); + }, + else => unreachable, + } + return input[index + 1..]; +} diff --git a/src/browser/tests/testing.js b/src/browser/tests/testing.js index d5ac8b97..5cb6220a 100644 --- a/src/browser/tests/testing.js +++ b/src/browser/tests/testing.js @@ -164,7 +164,17 @@ if (observed_ids[script_id] === 'fail') { return; } + observed_ids[script_id] = status; + + if (document.currentScript != null) { + if (document.currentScript.onerror === null) { + document.currentScript.onerror = function() { + observed_ids[document.currentScript.id] = 'fail'; + failed = true; + } + } + } } function _currentScriptId() { @@ -201,17 +211,15 @@ return `array: \n${value.map(_displayValue).join('\n')}\n`; } - // Quickjs can deal with cyclical objects, but browsers can't. We - // serialize with a custom replacer so that the tests can be run in browsers. const seen = []; return JSON.stringify(value, function(key, val) { - if (val != null && typeof val == "object") { - if (seen.indexOf(val) >= 0) { - return; - } - seen.push(val); - } - return val; -}); + if (val != null && typeof val == "object") { + if (seen.indexOf(val) >= 0) { + return; + } + seen.push(val); + } + return val; + }); } })(); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index b3847bc1..eadcff3d 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -336,13 +336,16 @@ pub fn setAttributeSafe(self: *Element, name: []const u8, value: []const u8, pag _ = try attributes.putSafe(name, value, self, page); } -fn getOrCreateAttributeList(self: *Element, page: *Page) !*Attribute.List { - return self._attributes orelse { - const a = try page.arena.create(Attribute.List); - a.* = .{}; - self._attributes = a; - return a; - }; +pub fn getOrCreateAttributeList(self: *Element, page: *Page) !*Attribute.List { + return self._attributes orelse return self.createAttributeList(page); +} + +pub fn createAttributeList(self: *Element, page: *Page) !*Attribute.List { + std.debug.assert(self._attributes == null); + const a = try page.arena.create(Attribute.List); + a.* = .{.normalize = self._namespace == .html}; + self._attributes = a; + return a; } pub fn getShadowRoot(self: *Element, page: *Page) ?*ShadowRoot { @@ -370,12 +373,7 @@ pub fn setAttributeNode(self: *Element, attr: *Attribute, page: *Page) !?*Attrib _ = try el.removeAttributeNode(attr, page); } - const attributes = self._attributes orelse blk: { - const a = try page.arena.create(Attribute.List); - a.* = .{}; - self._attributes = a; - break :blk a; - }; + const attributes = try self.getOrCreateAttributeList(page); return attributes.putAttribute(attr, self, page); } diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig index e1f55988..5b760d75 100644 --- a/src/browser/webapi/element/html/Script.zig +++ b/src/browser/webapi/element/html/Script.zig @@ -105,7 +105,7 @@ pub const JsApi = struct { pub const src = bridge.accessor(Script.getSrc, Script.setSrc, .{}); pub const @"type" = bridge.accessor(Script.getType, Script.setType, .{}); pub const onload = bridge.accessor(Script.getOnLoad, Script.setOnLoad, .{}); - pub const onerorr = bridge.accessor(Script.getOnError, Script.setOnError, .{}); + pub const onerror = bridge.accessor(Script.getOnError, Script.setOnError, .{}); pub const noModule = bridge.accessor(Script.getNoModule, null, .{}); }; diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 135bd6ad..a9b3c919 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -724,7 +724,7 @@ const IsolatedWorld = struct { // The isolate world must share at least some of the state with the related page, specifically the DocumentHTML // (assuming grantUniveralAccess will be set to True!). // We just created the world and the page. The page's state lives in the session, but is update on navigation. - // This also means this pointer becomes invalid after removePage untill a new page is created. + // This also means this pointer becomes invalid after removePage until a new page is created. // Currently we have only 1 page/frame and thus also only 1 state in the isolate world. pub fn createContext(self: *IsolatedWorld, page: *Page) !void { // if (self.executor.context != null) return error.Only1IsolatedContextSupported; diff --git a/src/http/Client.zig b/src/http/Client.zig index 1a646ea0..693d2fce 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -821,7 +821,7 @@ pub const Transfer = struct { self.deinit(); } - // abortAuthChallenge is called when an auth chanllenge interception is + // abortAuthChallenge is called when an auth challenge interception is // abort. We don't call self.client.endTransfer here b/c it has been done // before interception process. pub fn abortAuthChallenge(self: *Transfer) void {