Skip to content

Commit e4f8909

Browse files
committed
add Range.intersectsNode and cover a few more edge-cases
1 parent d51a03f commit e4f8909

File tree

2 files changed

+118
-77
lines changed

2 files changed

+118
-77
lines changed

src/browser/dom/node.zig

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,22 @@ pub const Node = struct {
267267
}
268268

269269
pub fn _compareDocumentPosition(self: *parser.Node, other: *parser.Node) !u32 {
270-
if (self == other) return 0;
270+
if (self == other) {
271+
return 0;
272+
}
271273

272-
const docself = try parser.nodeOwnerDocument(self);
273-
const docother = try parser.nodeOwnerDocument(other);
274+
const docself = try parser.nodeOwnerDocument(self) orelse blk: {
275+
if (try parser.nodeType(self) == .document) {
276+
break :blk @as(*parser.Document, @ptrCast(self));
277+
}
278+
break :blk null;
279+
};
280+
const docother = try parser.nodeOwnerDocument(other) orelse blk: {
281+
if (try parser.nodeType(other) == .document) {
282+
break :blk @as(*parser.Document, @ptrCast(other));
283+
}
284+
break :blk null;
285+
};
274286

275287
// Both are in different document.
276288
if (docself == null or docother == null or docself.? != docother.?) {
@@ -279,6 +291,13 @@ pub const Node = struct {
279291
@intFromEnum(parser.DocumentPosition.preceding);
280292
}
281293

294+
if (@intFromPtr(self) == @intFromPtr(docself.?)) {
295+
// if self is the document, and we already know other is in the
296+
// document, then other is contained by and following self.
297+
return @intFromEnum(parser.DocumentPosition.following) +
298+
@intFromEnum(parser.DocumentPosition.contained_by);
299+
}
300+
282301
const rootself = try parser.nodeGetRootNode(self);
283302
const rootother = try parser.nodeGetRootNode(other);
284303
if (rootself != rootother) {

src/browser/dom/range.zig

Lines changed: 96 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,10 @@ pub const Range = struct {
8585
}
8686

8787
pub fn _setStart(self: *Range, node: *parser.Node, offset_: i32) !void {
88-
const relative = self._comparePoint(node, offset_) catch |err| switch (err) {
88+
try ensureValidOffset(node, offset_);
89+
const offset: u32 = @intCast(offset_);
90+
const position = compare(node, offset, self.proto.start_container, self.proto.start_offset) catch |err| switch (err) {
8991
error.WrongDocument => blk: {
90-
// comparePoint doesn't check this on WrongDocument.
91-
try ensureValidOffset(node, offset_);
92-
9392
// allow a node with a different root than the current, or
9493
// a disconnected one. Treat it as if it's "after", so that
9594
// we also update the end_offset and end_container.
@@ -98,8 +97,7 @@ pub const Range = struct {
9897
else => return err,
9998
};
10099

101-
const offset: u32 = @intCast(offset_);
102-
if (relative == 1) {
100+
if (position == 1) {
103101
// if we're setting the node after the current start, the end must
104102
// be set too.
105103
self.proto.end_offset = offset;
@@ -123,11 +121,11 @@ pub const Range = struct {
123121
}
124122

125123
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_);
124+
try ensureValidOffset(node, offset_);
125+
const offset: u32 = @intCast(offset_);
130126

127+
const position = compare(node, offset, self.proto.start_container, self.proto.start_offset) catch |err| switch (err) {
128+
error.WrongDocument => blk: {
131129
// allow a node with a different root than the current, or
132130
// a disconnected one. Treat it as if it's "before", so that
133131
// we also update the end_offset and end_container.
@@ -136,10 +134,9 @@ pub const Range = struct {
136134
else => return err,
137135
};
138136

139-
const offset: u32 = @intCast(offset_);
140-
if (relative == -1) {
137+
if (position == -1) {
141138
// if we're setting the node before the current start, the start
142-
// must be
139+
// must be set too.
143140
self.proto.start_offset = offset;
144141
self.proto.start_container = node;
145142
}
@@ -207,101 +204,73 @@ pub const Range = struct {
207204
};
208205
}
209206

210-
pub fn _comparePoint(self: *const Range, ref_node: *parser.Node, offset_: i32) !i32 {
207+
pub fn _comparePoint(self: *const Range, node: *parser.Node, offset_: i32) !i32 {
211208
const start = self.proto.start_container;
212-
if (try parser.nodeGetRootNode(start) != try parser.nodeGetRootNode(ref_node)) {
209+
if (try parser.nodeGetRootNode(start) != try parser.nodeGetRootNode(node)) {
213210
// WPT really wants this error to be first. Later, when we check
214211
// if the relative position is 'disconnected', it'll also catch this
215212
// case, but WPT will complain because it sometimes also sends
216213
// invalid offsets, and it wants WrongDocument to be raised.
217214
return error.WrongDocument;
218215
}
219216

220-
if (try parser.nodeType(ref_node) == .document_type) {
217+
if (try parser.nodeType(node) == .document_type) {
221218
return error.InvalidNodeType;
222219
}
223220

224-
try ensureValidOffset(ref_node, offset_);
221+
try ensureValidOffset(node, offset_);
225222

226223
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)) {
224+
if (try compare(node, offset, start, self.proto.start_offset) == -1) {
256225
return -1;
257226
}
258227

259-
if (relative & @intFromEnum(parser.DocumentPosition.following) == @intFromEnum(parser.DocumentPosition.following)) {
228+
if (try compare(node, offset, self.proto.end_container, self.proto.end_offset) == 1) {
260229
return 1;
261230
}
262231

263-
// DUNNO
264-
// unreachable??
265232
return 0;
266233
}
267234

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) {
235+
pub fn _isPointInRange(self: *const Range, node: *parser.Node, offset_: i32) !bool {
236+
return self._comparePoint(node, offset_) catch |err| switch (err) {
270237
error.WrongDocument => return false,
271238
else => return err,
272239
} == 0;
273240
}
274241

242+
pub fn _intersectsNode(self: *const Range, node: *parser.Node) !bool {
243+
const start_root = try parser.nodeGetRootNode(self.proto.start_container);
244+
const node_root = try parser.nodeGetRootNode(node);
245+
if (start_root != node_root) {
246+
return false;
247+
}
248+
249+
const parent, const index = getParentAndIndex(node) catch |err| switch (err) {
250+
error.InvalidNodeType => return true, // if node has no parent, we return true.
251+
else => return err,
252+
};
253+
254+
if (try compare(parent, index + 1, self.proto.start_container, self.proto.start_offset) != 1) {
255+
// node isn't after start, can't intersect
256+
return false;
257+
}
258+
259+
if (try compare(parent, index, self.proto.end_container, self.proto.end_offset) != -1) {
260+
// node isn't before end, can't intersect
261+
return false;
262+
}
263+
264+
return true;
265+
}
266+
275267
// The Range.detach() method does nothing. It used to disable the Range
276268
// object and enable the browser to release associated resources. The
277269
// method has been kept for compatibility.
278270
// https://developer.mozilla.org/en-US/docs/Web/API/Range/detach
279271
pub fn _detach(_: *Range) void {}
280272
};
281273

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-
305274
fn ensureValidOffset(node: *parser.Node, offset: i32) !void {
306275
if (offset < 0) {
307276
return error.IndexSize;
@@ -347,6 +316,59 @@ fn getParentAndIndex(child: *parser.Node) !struct { *parser.Node, u32 } {
347316
return error.InvalidNodeType;
348317
}
349318

319+
// implementation is largely copied from the WPT helper called getPosition in
320+
// the common.js of the dom folder.
321+
fn compare(node_a: *parser.Node, offset_a: u32, node_b: *parser.Node, offset_b: u32) !i32 {
322+
if (node_a == node_b) {
323+
// This is a simple and common case, where the two nodes are the same
324+
// We just need to compare their offsets
325+
if (offset_a == offset_b) {
326+
return 0;
327+
}
328+
return if (offset_a < offset_b) -1 else 1;
329+
}
330+
331+
// We're probably comparing two different nodes. "Probably", because the
332+
// above case on considered the offset if the two nodes were the same
333+
// as-is. They could still be the same here, if we first consider the
334+
// offset.
335+
const position = try Node._compareDocumentPosition(node_b, node_a);
336+
if (position & @intFromEnum(parser.DocumentPosition.disconnected) == @intFromEnum(parser.DocumentPosition.disconnected)) {
337+
return error.WrongDocument;
338+
}
339+
340+
if (position & @intFromEnum(parser.DocumentPosition.following) == @intFromEnum(parser.DocumentPosition.following)) {
341+
return switch (try compare(node_b, offset_b, node_a, offset_a)) {
342+
-1 => 1,
343+
1 => -1,
344+
else => unreachable,
345+
};
346+
}
347+
348+
if (position & @intFromEnum(parser.DocumentPosition.contains) == @intFromEnum(parser.DocumentPosition.contains)) {
349+
// node_a contains node_b
350+
var child = node_b;
351+
while (try parser.nodeParentNode(child)) |parent| {
352+
if (parent == node_a) {
353+
// child.parentNode == node_a
354+
break;
355+
}
356+
child = parent;
357+
} else {
358+
// this should not happen, because Node._compareDocumentPosition
359+
// has told us that node_a contains node_b, so one of node_b's
360+
// parent's MUST be node_a. But somehow we do end up here sometimes.
361+
return -1;
362+
}
363+
364+
const child_parent, const child_index = try getParentAndIndex(child);
365+
std.debug.assert(node_a == child_parent);
366+
return if (child_index < offset_a) -1 else 1;
367+
}
368+
369+
return -1;
370+
}
371+
350372
const testing = @import("../../testing.zig");
351373
test "Browser.Range" {
352374
var runner = try testing.jsRunner(testing.tracking_allocator, .{});

0 commit comments

Comments
 (0)