Skip to content

Commit d51a03f

Browse files
committed
Improve correctness of Node.compareDocumentPosition and Range api.
Should fix a good chunk (~20K I think) of the recently broken WPT tests.
1 parent d4d8773 commit d51a03f

File tree

5 files changed

+252
-24
lines changed

5 files changed

+252
-24
lines changed

src/browser/dom/exceptions.zig

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,23 +68,24 @@ pub const DOMException = struct {
6868
}
6969

7070
// TODO: deinit
71-
pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) !DOMException {
72-
const errCast = @as(parser.DOMError, @errorCast(err));
73-
const errName = DOMException.name(errCast);
74-
const str = switch (errCast) {
71+
pub fn init(alloc: std.mem.Allocator, err: anyerror, caller_name: []const u8) !DOMException {
72+
const dom_error = @as(parser.DOMError, @errorCast(err));
73+
const error_name = DOMException.name(dom_error);
74+
const str = switch (dom_error) {
7575
error.HierarchyRequest => try allocPrint(
7676
alloc,
7777
"{s}: Failed to execute '{s}' on 'Node': The new child element contains the parent.",
78-
.{ errName, callerName },
78+
.{ error_name, caller_name },
7979
),
80-
error.NoError => unreachable,
80+
// todo add more custom error messages
8181
else => try allocPrint(
8282
alloc,
83-
"{s}: TODO message", // TODO: implement other messages
84-
.{DOMException.name(errCast)},
83+
"{s}: Failed to execute '{s}' : {s}",
84+
.{ error_name, caller_name, error_name },
8585
),
86+
error.NoError => unreachable,
8687
};
87-
return .{ .err = errCast, .str = str };
88+
return .{ .err = dom_error, .str = str };
8889
}
8990

9091
fn error_from_str(name_: []const u8) ?parser.DOMError {

src/browser/dom/node.zig

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ pub const Node = struct {
107107
pub const _ENTITY_NODE = @intFromEnum(parser.NodeType.entity);
108108
pub const _NOTATION_NODE = @intFromEnum(parser.NodeType.notation);
109109

110+
pub const _DOCUMENT_POSITION_DISCONNECTED = @intFromEnum(parser.DocumentPosition.disconnected);
111+
pub const _DOCUMENT_POSITION_PRECEDING = @intFromEnum(parser.DocumentPosition.preceding);
112+
pub const _DOCUMENT_POSITION_FOLLOWING = @intFromEnum(parser.DocumentPosition.following);
113+
pub const _DOCUMENT_POSITION_CONTAINS = @intFromEnum(parser.DocumentPosition.contains);
114+
pub const _DOCUMENT_POSITION_CONTAINED_BY = @intFromEnum(parser.DocumentPosition.contained_by);
115+
pub const _DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = @intFromEnum(parser.DocumentPosition.implementation_specific);
116+
110117
// JS funcs
111118
// --------
112119

@@ -266,8 +273,18 @@ pub const Node = struct {
266273
const docother = try parser.nodeOwnerDocument(other);
267274

268275
// Both are in different document.
269-
if (docself == null or docother == null or docother.? != docself.?) {
270-
return @intFromEnum(parser.DocumentPosition.disconnected);
276+
if (docself == null or docother == null or docself.? != docother.?) {
277+
return @intFromEnum(parser.DocumentPosition.disconnected) +
278+
@intFromEnum(parser.DocumentPosition.implementation_specific) +
279+
@intFromEnum(parser.DocumentPosition.preceding);
280+
}
281+
282+
const rootself = try parser.nodeGetRootNode(self);
283+
const rootother = try parser.nodeGetRootNode(other);
284+
if (rootself != rootother) {
285+
return @intFromEnum(parser.DocumentPosition.disconnected) +
286+
@intFromEnum(parser.DocumentPosition.implementation_specific) +
287+
@intFromEnum(parser.DocumentPosition.preceding);
271288
}
272289

273290
// TODO Both are in a different trees in the same document.

src/browser/dom/range.zig

Lines changed: 220 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ const std = @import("std");
2121
const parser = @import("../netsurf.zig");
2222
const Page = @import("../page.zig").Page;
2323

24-
const NodeUnion = @import("node.zig").Union;
2524
const Node = @import("node.zig").Node;
25+
const NodeUnion = @import("node.zig").Union;
26+
const DOMException = @import("exceptions.zig").DOMException;
2627

2728
pub const Interfaces = .{
2829
AbstractRange,
@@ -32,9 +33,9 @@ pub const Interfaces = .{
3233
pub const AbstractRange = struct {
3334
collapsed: bool,
3435
end_container: *parser.Node,
35-
end_offset: i32,
36+
end_offset: u32,
3637
start_container: *parser.Node,
37-
start_offset: i32,
38+
start_offset: u32,
3839

3940
pub fn updateCollapsed(self: *AbstractRange) void {
4041
// TODO: Eventually, compare properly.
@@ -49,20 +50,21 @@ pub const AbstractRange = struct {
4950
return Node.toInterface(self.end_container);
5051
}
5152

52-
pub fn get_endOffset(self: *const AbstractRange) i32 {
53+
pub fn get_endOffset(self: *const AbstractRange) u32 {
5354
return self.end_offset;
5455
}
5556

5657
pub fn get_startContainer(self: *const AbstractRange) !NodeUnion {
5758
return Node.toInterface(self.start_container);
5859
}
5960

60-
pub fn get_startOffset(self: *const AbstractRange) i32 {
61+
pub fn get_startOffset(self: *const AbstractRange) u32 {
6162
return self.start_offset;
6263
}
6364
};
6465

6566
pub const Range = struct {
67+
pub const Exception = DOMException;
6668
pub const prototype = *AbstractRange;
6769

6870
proto: AbstractRange,
@@ -82,18 +84,83 @@ pub const Range = struct {
8284
return .{ .proto = proto };
8385
}
8486

85-
pub fn _setStart(self: *Range, node: *parser.Node, offset: i32) void {
87+
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
88+
const relative = self._comparePoint(node, offset_) catch |err| switch (err) {
89+
error.WrongDocument => blk: {
90+
// comparePoint doesn't check this on WrongDocument.
91+
try ensureValidOffset(node, offset_);
92+
93+
// allow a node with a different root than the current, or
94+
// a disconnected one. Treat it as if it's "after", so that
95+
// we also update the end_offset and end_container.
96+
break :blk 1;
97+
},
98+
else => return err,
99+
};
100+
101+
const offset: u32 = @intCast(offset_);
102+
if (relative == 1) {
103+
// if we're setting the node after the current start, the end must
104+
// be set too.
105+
self.proto.end_offset = offset;
106+
self.proto.end_container = node;
107+
}
86108
self.proto.start_container = node;
87109
self.proto.start_offset = offset;
88110
self.proto.updateCollapsed();
89111
}
90112

91-
pub fn _setEnd(self: *Range, node: *parser.Node, offset: i32) void {
113+
pub fn _setStartBefore(self: *Range, node: *parser.Node) !void {
114+
const parent, const index = try getParentAndIndex(node);
115+
self.proto.start_container = parent;
116+
self.proto.start_offset = index;
117+
}
118+
119+
pub fn _setStartAfter(self: *Range, node: *parser.Node) !void {
120+
const parent, const index = try getParentAndIndex(node);
121+
self.proto.start_container = parent;
122+
self.proto.start_offset = index + 1;
123+
}
124+
125+
pub fn _setEnd(self: *Range, node: *parser.Node, offset_: i32) !void {
126+
const relative = self._comparePoint(node, offset_) catch |err| switch (err) {
127+
error.WrongDocument => blk: {
128+
// comparePoint doesn't check this on WrongDocument.
129+
try ensureValidOffset(node, offset_);
130+
131+
// allow a node with a different root than the current, or
132+
// a disconnected one. Treat it as if it's "before", so that
133+
// we also update the end_offset and end_container.
134+
break :blk -1;
135+
},
136+
else => return err,
137+
};
138+
139+
const offset: u32 = @intCast(offset_);
140+
if (relative == -1) {
141+
// if we're setting the node before the current start, the start
142+
// must be
143+
self.proto.start_offset = offset;
144+
self.proto.start_container = node;
145+
}
146+
92147
self.proto.end_container = node;
93148
self.proto.end_offset = offset;
94149
self.proto.updateCollapsed();
95150
}
96151

152+
pub fn _setEndBefore(self: *Range, node: *parser.Node) !void {
153+
const parent, const index = try getParentAndIndex(node);
154+
self.proto.end_container = parent;
155+
self.proto.end_offset = index;
156+
}
157+
158+
pub fn _setEndAfter(self: *Range, node: *parser.Node) !void {
159+
const parent, const index = try getParentAndIndex(node);
160+
self.proto.end_container = parent;
161+
self.proto.end_offset = index + 1;
162+
}
163+
97164
pub fn _createContextualFragment(_: *Range, fragment: []const u8, page: *Page) !*parser.DocumentFragment {
98165
const document_html = page.window.document;
99166
const document = parser.documentHTMLToDocument(document_html);
@@ -127,13 +194,159 @@ pub const Range = struct {
127194
self.proto.updateCollapsed();
128195
}
129196

197+
// creates a copy
198+
pub fn _cloneRange(self: *const Range) Range {
199+
return .{
200+
.proto = .{
201+
.collapsed = self.proto.collapsed,
202+
.end_container = self.proto.end_container,
203+
.end_offset = self.proto.end_offset,
204+
.start_container = self.proto.start_container,
205+
.start_offset = self.proto.start_offset,
206+
},
207+
};
208+
}
209+
210+
pub fn _comparePoint(self: *const Range, ref_node: *parser.Node, offset_: i32) !i32 {
211+
const start = self.proto.start_container;
212+
if (try parser.nodeGetRootNode(start) != try parser.nodeGetRootNode(ref_node)) {
213+
// WPT really wants this error to be first. Later, when we check
214+
// if the relative position is 'disconnected', it'll also catch this
215+
// case, but WPT will complain because it sometimes also sends
216+
// invalid offsets, and it wants WrongDocument to be raised.
217+
return error.WrongDocument;
218+
}
219+
220+
if (try parser.nodeType(ref_node) == .document_type) {
221+
return error.InvalidNodeType;
222+
}
223+
224+
try ensureValidOffset(ref_node, offset_);
225+
226+
const offset: u32 = @intCast(offset_);
227+
if (ref_node == start) {
228+
// This is a simple and common case, where the reference node and
229+
// our start node are the same, so we just have to compare the offsets
230+
const start_offset = self.proto.start_offset;
231+
if (offset == start_offset) {
232+
return 0;
233+
}
234+
return if (offset < start_offset) -1 else 1;
235+
}
236+
237+
// We're probably comparing two different nodes. "Probably", because the
238+
// above case on considered the offset if the two nodes were the same
239+
// as-is. They could still be the same here, if we first consider the
240+
// offset.
241+
// Furthermore, as far as I can tell, if either or both nodes are textual,
242+
// then we're doing a node comparison of their parents. This kind of
243+
// makes sense, one/two text nodes which aren't the same, can only
244+
// be positionally compared in relation to it/their parents.
245+
246+
const adjusted_start = try getNodeForCompare(start, self.proto.start_offset);
247+
const adjusted_ref_node = try getNodeForCompare(ref_node, offset);
248+
249+
const relative = try Node._compareDocumentPosition(adjusted_start, adjusted_ref_node);
250+
251+
if (relative & @intFromEnum(parser.DocumentPosition.disconnected) == @intFromEnum(parser.DocumentPosition.disconnected)) {
252+
return error.WrongDocument;
253+
}
254+
255+
if (relative & @intFromEnum(parser.DocumentPosition.preceding) == @intFromEnum(parser.DocumentPosition.preceding)) {
256+
return -1;
257+
}
258+
259+
if (relative & @intFromEnum(parser.DocumentPosition.following) == @intFromEnum(parser.DocumentPosition.following)) {
260+
return 1;
261+
}
262+
263+
// DUNNO
264+
// unreachable??
265+
return 0;
266+
}
267+
268+
pub fn _isPointInRange(self: *const Range, ref_node: *parser.Node, offset_: i32) !bool {
269+
return self._comparePoint(ref_node, offset_) catch |err| switch (err) {
270+
error.WrongDocument => return false,
271+
else => return err,
272+
} == 0;
273+
}
274+
130275
// The Range.detach() method does nothing. It used to disable the Range
131276
// object and enable the browser to release associated resources. The
132277
// method has been kept for compatibility.
133278
// https://developer.mozilla.org/en-US/docs/Web/API/Range/detach
134279
pub fn _detach(_: *Range) void {}
135280
};
136281

282+
fn getNodeForCompare(node: *parser.Node, offset: u32) !*parser.Node {
283+
if (try isTextual(node)) {
284+
// when we're comparing a text node to another node which is not the same
285+
// then we're really compare the position of the parent. It doesn't
286+
// matter if the other node is a text node itself or not, all that matters
287+
// is we're sure it isn't the same text node (because if they are the
288+
// same text node, then we're comparing the offset (character position)
289+
// of the text node)
290+
291+
// not sure this is the correct error
292+
return (try parser.nodeParentNode(node)) orelse return error.WrongDocument;
293+
}
294+
if (offset == 0) {
295+
return node;
296+
}
297+
298+
const children = try parser.nodeGetChildNodes(node);
299+
300+
// not sure about this error
301+
// - 1 because, while the offset is 0 based, 0 seems to represent the parent
302+
return (try parser.nodeListItem(children, offset - 1)) orelse error.IndexSize;
303+
}
304+
305+
fn ensureValidOffset(node: *parser.Node, offset: i32) !void {
306+
if (offset < 0) {
307+
return error.IndexSize;
308+
}
309+
310+
// not >= because 0 seems to represent the node itself.
311+
if (offset > try nodeLength(node)) {
312+
return error.IndexSize;
313+
}
314+
}
315+
316+
fn nodeLength(node: *parser.Node) !usize {
317+
switch (try isTextual(node)) {
318+
true => return ((try parser.nodeTextContent(node)) orelse "").len,
319+
false => {
320+
const children = try parser.nodeGetChildNodes(node);
321+
return @intCast(try parser.nodeListLength(children));
322+
},
323+
}
324+
}
325+
326+
fn isTextual(node: *parser.Node) !bool {
327+
return switch (try parser.nodeType(node)) {
328+
.text, .comment, .cdata_section => true,
329+
else => false,
330+
};
331+
}
332+
333+
fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } {
334+
const parent = (try parser.nodeParentNode(child)) orelse return error.InvalidNodeType;
335+
const children = try parser.nodeGetChildNodes(parent);
336+
const ln = try parser.nodeListLength(children);
337+
var i: u32 = 0;
338+
while (i < ln) {
339+
defer i += 1;
340+
const c = try parser.nodeListItem(children, i) orelse continue;
341+
if (c == child) {
342+
return .{ parent, i };
343+
}
344+
}
345+
346+
// should not be possible to reach this point
347+
return error.InvalidNodeType;
348+
}
349+
137350
const testing = @import("../../testing.zig");
138351
test "Browser.Range" {
139352
var runner = try testing.jsRunner(testing.tracking_allocator, .{});

src/main_wpt.zig

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
const std = @import("std");
2020

21+
const log = @import("log.zig");
2122
const Allocator = std.mem.Allocator;
2223
const ArenaAllocator = std.heap.ArenaAllocator;
2324

@@ -29,11 +30,6 @@ const polyfill = @import("browser/polyfill/polyfill.zig");
2930

3031
const WPT_DIR = "tests/wpt";
3132

32-
pub const std_options = std.Options{
33-
// Set the log level to info
34-
.log_level = .info,
35-
};
36-
3733
// TODO For now the WPT tests run is specific to WPT.
3834
// It manually load js framwork libs, and run the first script w/ js content in
3935
// the HTML page.
@@ -43,6 +39,7 @@ pub fn main() !void {
4339
var gpa: std.heap.DebugAllocator(.{}) = .init;
4440
defer _ = gpa.deinit();
4541
const allocator = gpa.allocator();
42+
log.opts.level = .warn;
4643

4744
// An arena for the runner itself, lives for the duration of the the process
4845
var ra = ArenaAllocator.init(allocator);

src/runtime/js.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3866,7 +3866,7 @@ const NamedFunction = struct {
38663866
// this can add as much as 10 seconds of compilation time.
38673867
fn logFunctionCallError(arena: Allocator, isolate: v8.Isolate, context: v8.Context, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void {
38683868
const args_dump = serializeFunctionArgs(arena, isolate, context, info) catch "failed to serialize args";
3869-
log.warn(.js, "function call error", .{
3869+
log.info(.js, "function call error", .{
38703870
.name = function_name,
38713871
.err = err,
38723872
.args = args_dump,

0 commit comments

Comments
 (0)