mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 23:23:28 +00:00
Improve correctness of Node.compareDocumentPosition and Range api.
Should fix a good chunk (~20K I think) of the recently broken WPT tests.
This commit is contained in:
@@ -68,23 +68,24 @@ pub const DOMException = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: deinit
|
// TODO: deinit
|
||||||
pub fn init(alloc: std.mem.Allocator, err: anyerror, callerName: []const u8) !DOMException {
|
pub fn init(alloc: std.mem.Allocator, err: anyerror, caller_name: []const u8) !DOMException {
|
||||||
const errCast = @as(parser.DOMError, @errorCast(err));
|
const dom_error = @as(parser.DOMError, @errorCast(err));
|
||||||
const errName = DOMException.name(errCast);
|
const error_name = DOMException.name(dom_error);
|
||||||
const str = switch (errCast) {
|
const str = switch (dom_error) {
|
||||||
error.HierarchyRequest => try allocPrint(
|
error.HierarchyRequest => try allocPrint(
|
||||||
alloc,
|
alloc,
|
||||||
"{s}: Failed to execute '{s}' on 'Node': The new child element contains the parent.",
|
"{s}: Failed to execute '{s}' on 'Node': The new child element contains the parent.",
|
||||||
.{ errName, callerName },
|
.{ error_name, caller_name },
|
||||||
),
|
),
|
||||||
error.NoError => unreachable,
|
// todo add more custom error messages
|
||||||
else => try allocPrint(
|
else => try allocPrint(
|
||||||
alloc,
|
alloc,
|
||||||
"{s}: TODO message", // TODO: implement other messages
|
"{s}: Failed to execute '{s}' : {s}",
|
||||||
.{DOMException.name(errCast)},
|
.{ error_name, caller_name, error_name },
|
||||||
),
|
),
|
||||||
|
error.NoError => unreachable,
|
||||||
};
|
};
|
||||||
return .{ .err = errCast, .str = str };
|
return .{ .err = dom_error, .str = str };
|
||||||
}
|
}
|
||||||
|
|
||||||
fn error_from_str(name_: []const u8) ?parser.DOMError {
|
fn error_from_str(name_: []const u8) ?parser.DOMError {
|
||||||
|
|||||||
@@ -107,6 +107,13 @@ pub const Node = struct {
|
|||||||
pub const _ENTITY_NODE = @intFromEnum(parser.NodeType.entity);
|
pub const _ENTITY_NODE = @intFromEnum(parser.NodeType.entity);
|
||||||
pub const _NOTATION_NODE = @intFromEnum(parser.NodeType.notation);
|
pub const _NOTATION_NODE = @intFromEnum(parser.NodeType.notation);
|
||||||
|
|
||||||
|
pub const _DOCUMENT_POSITION_DISCONNECTED = @intFromEnum(parser.DocumentPosition.disconnected);
|
||||||
|
pub const _DOCUMENT_POSITION_PRECEDING = @intFromEnum(parser.DocumentPosition.preceding);
|
||||||
|
pub const _DOCUMENT_POSITION_FOLLOWING = @intFromEnum(parser.DocumentPosition.following);
|
||||||
|
pub const _DOCUMENT_POSITION_CONTAINS = @intFromEnum(parser.DocumentPosition.contains);
|
||||||
|
pub const _DOCUMENT_POSITION_CONTAINED_BY = @intFromEnum(parser.DocumentPosition.contained_by);
|
||||||
|
pub const _DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = @intFromEnum(parser.DocumentPosition.implementation_specific);
|
||||||
|
|
||||||
// JS funcs
|
// JS funcs
|
||||||
// --------
|
// --------
|
||||||
|
|
||||||
@@ -266,8 +273,18 @@ pub const Node = struct {
|
|||||||
const docother = try parser.nodeOwnerDocument(other);
|
const docother = try parser.nodeOwnerDocument(other);
|
||||||
|
|
||||||
// Both are in different document.
|
// Both are in different document.
|
||||||
if (docself == null or docother == null or docother.? != docself.?) {
|
if (docself == null or docother == null or docself.? != docother.?) {
|
||||||
return @intFromEnum(parser.DocumentPosition.disconnected);
|
return @intFromEnum(parser.DocumentPosition.disconnected) +
|
||||||
|
@intFromEnum(parser.DocumentPosition.implementation_specific) +
|
||||||
|
@intFromEnum(parser.DocumentPosition.preceding);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootself = try parser.nodeGetRootNode(self);
|
||||||
|
const rootother = try parser.nodeGetRootNode(other);
|
||||||
|
if (rootself != rootother) {
|
||||||
|
return @intFromEnum(parser.DocumentPosition.disconnected) +
|
||||||
|
@intFromEnum(parser.DocumentPosition.implementation_specific) +
|
||||||
|
@intFromEnum(parser.DocumentPosition.preceding);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Both are in a different trees in the same document.
|
// TODO Both are in a different trees in the same document.
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ const std = @import("std");
|
|||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
|
|
||||||
const NodeUnion = @import("node.zig").Union;
|
|
||||||
const Node = @import("node.zig").Node;
|
const Node = @import("node.zig").Node;
|
||||||
|
const NodeUnion = @import("node.zig").Union;
|
||||||
|
const DOMException = @import("exceptions.zig").DOMException;
|
||||||
|
|
||||||
pub const Interfaces = .{
|
pub const Interfaces = .{
|
||||||
AbstractRange,
|
AbstractRange,
|
||||||
@@ -32,9 +33,9 @@ pub const Interfaces = .{
|
|||||||
pub const AbstractRange = struct {
|
pub const AbstractRange = struct {
|
||||||
collapsed: bool,
|
collapsed: bool,
|
||||||
end_container: *parser.Node,
|
end_container: *parser.Node,
|
||||||
end_offset: i32,
|
end_offset: u32,
|
||||||
start_container: *parser.Node,
|
start_container: *parser.Node,
|
||||||
start_offset: i32,
|
start_offset: u32,
|
||||||
|
|
||||||
pub fn updateCollapsed(self: *AbstractRange) void {
|
pub fn updateCollapsed(self: *AbstractRange) void {
|
||||||
// TODO: Eventually, compare properly.
|
// TODO: Eventually, compare properly.
|
||||||
@@ -49,7 +50,7 @@ pub const AbstractRange = struct {
|
|||||||
return Node.toInterface(self.end_container);
|
return Node.toInterface(self.end_container);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_endOffset(self: *const AbstractRange) i32 {
|
pub fn get_endOffset(self: *const AbstractRange) u32 {
|
||||||
return self.end_offset;
|
return self.end_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,12 +58,13 @@ pub const AbstractRange = struct {
|
|||||||
return Node.toInterface(self.start_container);
|
return Node.toInterface(self.start_container);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_startOffset(self: *const AbstractRange) i32 {
|
pub fn get_startOffset(self: *const AbstractRange) u32 {
|
||||||
return self.start_offset;
|
return self.start_offset;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Range = struct {
|
pub const Range = struct {
|
||||||
|
pub const Exception = DOMException;
|
||||||
pub const prototype = *AbstractRange;
|
pub const prototype = *AbstractRange;
|
||||||
|
|
||||||
proto: AbstractRange,
|
proto: AbstractRange,
|
||||||
@@ -82,18 +84,83 @@ pub const Range = struct {
|
|||||||
return .{ .proto = proto };
|
return .{ .proto = proto };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _setStart(self: *Range, node: *parser.Node, offset: i32) void {
|
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
|
||||||
|
const relative = self._comparePoint(node, offset_) catch |err| switch (err) {
|
||||||
|
error.WrongDocument => blk: {
|
||||||
|
// comparePoint doesn't check this on WrongDocument.
|
||||||
|
try ensureValidOffset(node, offset_);
|
||||||
|
|
||||||
|
// allow a node with a different root than the current, or
|
||||||
|
// a disconnected one. Treat it as if it's "after", so that
|
||||||
|
// we also update the end_offset and end_container.
|
||||||
|
break :blk 1;
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
|
|
||||||
|
const offset: u32 = @intCast(offset_);
|
||||||
|
if (relative == 1) {
|
||||||
|
// if we're setting the node after the current start, the end must
|
||||||
|
// be set too.
|
||||||
|
self.proto.end_offset = offset;
|
||||||
|
self.proto.end_container = node;
|
||||||
|
}
|
||||||
self.proto.start_container = node;
|
self.proto.start_container = node;
|
||||||
self.proto.start_offset = offset;
|
self.proto.start_offset = offset;
|
||||||
self.proto.updateCollapsed();
|
self.proto.updateCollapsed();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _setEnd(self: *Range, node: *parser.Node, offset: i32) void {
|
pub fn _setStartBefore(self: *Range, node: *parser.Node) !void {
|
||||||
|
const parent, const index = try getParentAndIndex(node);
|
||||||
|
self.proto.start_container = parent;
|
||||||
|
self.proto.start_offset = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _setStartAfter(self: *Range, node: *parser.Node) !void {
|
||||||
|
const parent, const index = try getParentAndIndex(node);
|
||||||
|
self.proto.start_container = parent;
|
||||||
|
self.proto.start_offset = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _setEnd(self: *Range, node: *parser.Node, offset_: i32) !void {
|
||||||
|
const relative = self._comparePoint(node, offset_) catch |err| switch (err) {
|
||||||
|
error.WrongDocument => blk: {
|
||||||
|
// comparePoint doesn't check this on WrongDocument.
|
||||||
|
try ensureValidOffset(node, offset_);
|
||||||
|
|
||||||
|
// allow a node with a different root than the current, or
|
||||||
|
// a disconnected one. Treat it as if it's "before", so that
|
||||||
|
// we also update the end_offset and end_container.
|
||||||
|
break :blk -1;
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
};
|
||||||
|
|
||||||
|
const offset: u32 = @intCast(offset_);
|
||||||
|
if (relative == -1) {
|
||||||
|
// if we're setting the node before the current start, the start
|
||||||
|
// must be
|
||||||
|
self.proto.start_offset = offset;
|
||||||
|
self.proto.start_container = node;
|
||||||
|
}
|
||||||
|
|
||||||
self.proto.end_container = node;
|
self.proto.end_container = node;
|
||||||
self.proto.end_offset = offset;
|
self.proto.end_offset = offset;
|
||||||
self.proto.updateCollapsed();
|
self.proto.updateCollapsed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn _setEndBefore(self: *Range, node: *parser.Node) !void {
|
||||||
|
const parent, const index = try getParentAndIndex(node);
|
||||||
|
self.proto.end_container = parent;
|
||||||
|
self.proto.end_offset = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _setEndAfter(self: *Range, node: *parser.Node) !void {
|
||||||
|
const parent, const index = try getParentAndIndex(node);
|
||||||
|
self.proto.end_container = parent;
|
||||||
|
self.proto.end_offset = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn _createContextualFragment(_: *Range, fragment: []const u8, page: *Page) !*parser.DocumentFragment {
|
pub fn _createContextualFragment(_: *Range, fragment: []const u8, page: *Page) !*parser.DocumentFragment {
|
||||||
const document_html = page.window.document;
|
const document_html = page.window.document;
|
||||||
const document = parser.documentHTMLToDocument(document_html);
|
const document = parser.documentHTMLToDocument(document_html);
|
||||||
@@ -127,6 +194,84 @@ pub const Range = struct {
|
|||||||
self.proto.updateCollapsed();
|
self.proto.updateCollapsed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// creates a copy
|
||||||
|
pub fn _cloneRange(self: *const Range) Range {
|
||||||
|
return .{
|
||||||
|
.proto = .{
|
||||||
|
.collapsed = self.proto.collapsed,
|
||||||
|
.end_container = self.proto.end_container,
|
||||||
|
.end_offset = self.proto.end_offset,
|
||||||
|
.start_container = self.proto.start_container,
|
||||||
|
.start_offset = self.proto.start_offset,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _comparePoint(self: *const Range, ref_node: *parser.Node, offset_: i32) !i32 {
|
||||||
|
const start = self.proto.start_container;
|
||||||
|
if (try parser.nodeGetRootNode(start) != try parser.nodeGetRootNode(ref_node)) {
|
||||||
|
// WPT really wants this error to be first. Later, when we check
|
||||||
|
// if the relative position is 'disconnected', it'll also catch this
|
||||||
|
// case, but WPT will complain because it sometimes also sends
|
||||||
|
// invalid offsets, and it wants WrongDocument to be raised.
|
||||||
|
return error.WrongDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (try parser.nodeType(ref_node) == .document_type) {
|
||||||
|
return error.InvalidNodeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
try ensureValidOffset(ref_node, offset_);
|
||||||
|
|
||||||
|
const offset: u32 = @intCast(offset_);
|
||||||
|
if (ref_node == start) {
|
||||||
|
// This is a simple and common case, where the reference node and
|
||||||
|
// our start node are the same, so we just have to compare the offsets
|
||||||
|
const start_offset = self.proto.start_offset;
|
||||||
|
if (offset == start_offset) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return if (offset < start_offset) -1 else 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're probably comparing two different nodes. "Probably", because the
|
||||||
|
// above case on considered the offset if the two nodes were the same
|
||||||
|
// as-is. They could still be the same here, if we first consider the
|
||||||
|
// offset.
|
||||||
|
// Furthermore, as far as I can tell, if either or both nodes are textual,
|
||||||
|
// then we're doing a node comparison of their parents. This kind of
|
||||||
|
// makes sense, one/two text nodes which aren't the same, can only
|
||||||
|
// be positionally compared in relation to it/their parents.
|
||||||
|
|
||||||
|
const adjusted_start = try getNodeForCompare(start, self.proto.start_offset);
|
||||||
|
const adjusted_ref_node = try getNodeForCompare(ref_node, offset);
|
||||||
|
|
||||||
|
const relative = try Node._compareDocumentPosition(adjusted_start, adjusted_ref_node);
|
||||||
|
|
||||||
|
if (relative & @intFromEnum(parser.DocumentPosition.disconnected) == @intFromEnum(parser.DocumentPosition.disconnected)) {
|
||||||
|
return error.WrongDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relative & @intFromEnum(parser.DocumentPosition.preceding) == @intFromEnum(parser.DocumentPosition.preceding)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relative & @intFromEnum(parser.DocumentPosition.following) == @intFromEnum(parser.DocumentPosition.following)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DUNNO
|
||||||
|
// unreachable??
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _isPointInRange(self: *const Range, ref_node: *parser.Node, offset_: i32) !bool {
|
||||||
|
return self._comparePoint(ref_node, offset_) catch |err| switch (err) {
|
||||||
|
error.WrongDocument => return false,
|
||||||
|
else => return err,
|
||||||
|
} == 0;
|
||||||
|
}
|
||||||
|
|
||||||
// The Range.detach() method does nothing. It used to disable the Range
|
// The Range.detach() method does nothing. It used to disable the Range
|
||||||
// object and enable the browser to release associated resources. The
|
// object and enable the browser to release associated resources. The
|
||||||
// method has been kept for compatibility.
|
// method has been kept for compatibility.
|
||||||
@@ -134,6 +279,74 @@ pub const Range = struct {
|
|||||||
pub fn _detach(_: *Range) void {}
|
pub fn _detach(_: *Range) void {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn getNodeForCompare(node: *parser.Node, offset: u32) !*parser.Node {
|
||||||
|
if (try isTextual(node)) {
|
||||||
|
// when we're comparing a text node to another node which is not the same
|
||||||
|
// then we're really compare the position of the parent. It doesn't
|
||||||
|
// matter if the other node is a text node itself or not, all that matters
|
||||||
|
// is we're sure it isn't the same text node (because if they are the
|
||||||
|
// same text node, then we're comparing the offset (character position)
|
||||||
|
// of the text node)
|
||||||
|
|
||||||
|
// not sure this is the correct error
|
||||||
|
return (try parser.nodeParentNode(node)) orelse return error.WrongDocument;
|
||||||
|
}
|
||||||
|
if (offset == 0) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = try parser.nodeGetChildNodes(node);
|
||||||
|
|
||||||
|
// not sure about this error
|
||||||
|
// - 1 because, while the offset is 0 based, 0 seems to represent the parent
|
||||||
|
return (try parser.nodeListItem(children, offset - 1)) orelse error.IndexSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensureValidOffset(node: *parser.Node, offset: i32) !void {
|
||||||
|
if (offset < 0) {
|
||||||
|
return error.IndexSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not >= because 0 seems to represent the node itself.
|
||||||
|
if (offset > try nodeLength(node)) {
|
||||||
|
return error.IndexSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nodeLength(node: *parser.Node) !usize {
|
||||||
|
switch (try isTextual(node)) {
|
||||||
|
true => return ((try parser.nodeTextContent(node)) orelse "").len,
|
||||||
|
false => {
|
||||||
|
const children = try parser.nodeGetChildNodes(node);
|
||||||
|
return @intCast(try parser.nodeListLength(children));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isTextual(node: *parser.Node) !bool {
|
||||||
|
return switch (try parser.nodeType(node)) {
|
||||||
|
.text, .comment, .cdata_section => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } {
|
||||||
|
const parent = (try parser.nodeParentNode(child)) orelse return error.InvalidNodeType;
|
||||||
|
const children = try parser.nodeGetChildNodes(parent);
|
||||||
|
const ln = try parser.nodeListLength(children);
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < ln) {
|
||||||
|
defer i += 1;
|
||||||
|
const c = try parser.nodeListItem(children, i) orelse continue;
|
||||||
|
if (c == child) {
|
||||||
|
return .{ parent, i };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// should not be possible to reach this point
|
||||||
|
return error.InvalidNodeType;
|
||||||
|
}
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
test "Browser.Range" {
|
test "Browser.Range" {
|
||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const log = @import("log.zig");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
@@ -29,11 +30,6 @@ const polyfill = @import("browser/polyfill/polyfill.zig");
|
|||||||
|
|
||||||
const WPT_DIR = "tests/wpt";
|
const WPT_DIR = "tests/wpt";
|
||||||
|
|
||||||
pub const std_options = std.Options{
|
|
||||||
// Set the log level to info
|
|
||||||
.log_level = .info,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO For now the WPT tests run is specific to WPT.
|
// TODO For now the WPT tests run is specific to WPT.
|
||||||
// It manually load js framwork libs, and run the first script w/ js content in
|
// It manually load js framwork libs, and run the first script w/ js content in
|
||||||
// the HTML page.
|
// the HTML page.
|
||||||
@@ -43,6 +39,7 @@ pub fn main() !void {
|
|||||||
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
defer _ = gpa.deinit();
|
defer _ = gpa.deinit();
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
log.opts.level = .warn;
|
||||||
|
|
||||||
// An arena for the runner itself, lives for the duration of the the process
|
// An arena for the runner itself, lives for the duration of the the process
|
||||||
var ra = ArenaAllocator.init(allocator);
|
var ra = ArenaAllocator.init(allocator);
|
||||||
|
|||||||
@@ -3866,7 +3866,7 @@ const NamedFunction = struct {
|
|||||||
// this can add as much as 10 seconds of compilation time.
|
// this can add as much as 10 seconds of compilation time.
|
||||||
fn logFunctionCallError(arena: Allocator, isolate: v8.Isolate, context: v8.Context, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void {
|
fn logFunctionCallError(arena: Allocator, isolate: v8.Isolate, context: v8.Context, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void {
|
||||||
const args_dump = serializeFunctionArgs(arena, isolate, context, info) catch "failed to serialize args";
|
const args_dump = serializeFunctionArgs(arena, isolate, context, info) catch "failed to serialize args";
|
||||||
log.warn(.js, "function call error", .{
|
log.info(.js, "function call error", .{
|
||||||
.name = function_name,
|
.name = function_name,
|
||||||
.err = err,
|
.err = err,
|
||||||
.args = args_dump,
|
.args = args_dump,
|
||||||
|
|||||||
Reference in New Issue
Block a user