diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 95433ce1..d053f0ab 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -2907,23 +2907,7 @@ pub fn updateRangesForCharacterDataReplace(self: *Page, target: *Node, offset: u var it: ?*std.DoublyLinkedList.Node = self._live_ranges.first; while (it) |link| : (it = link.next) { const ar: *AbstractRange = @fieldParentPtr("_range_link", link); - - if (ar._start_container == target) { - if (ar._start_offset > offset and ar._start_offset <= offset + count) { - ar._start_offset = offset; - } else if (ar._start_offset > offset + count) { - // Use i64 intermediate to avoid u32 underflow when count > data_len - ar._start_offset = @intCast(@as(i64, ar._start_offset) + @as(i64, data_len) - @as(i64, count)); - } - } - - if (ar._end_container == target) { - if (ar._end_offset > offset and ar._end_offset <= offset + count) { - ar._end_offset = offset; - } else if (ar._end_offset > offset + count) { - ar._end_offset = @intCast(@as(i64, ar._end_offset) + @as(i64, data_len) - @as(i64, count)); - } - } + ar.updateForCharacterDataReplace(target, offset, count, data_len); } } @@ -2936,25 +2920,7 @@ pub fn updateRangesForSplitText(self: *Page, target: *Node, new_node: *Node, off var it: ?*std.DoublyLinkedList.Node = self._live_ranges.first; while (it) |link| : (it = link.next) { const ar: *AbstractRange = @fieldParentPtr("_range_link", link); - - // Step 7b: ranges on the original node with start > offset move to new node - if (ar._start_container == target and ar._start_offset > offset) { - ar._start_container = new_node; - ar._start_offset = ar._start_offset - offset; - } - // Step 7c: ranges on the original node with end > offset move to new node - if (ar._end_container == target and ar._end_offset > offset) { - ar._end_container = new_node; - ar._end_offset = ar._end_offset - offset; - } - // Step 7d: ranges on parent with start == node_index + 1 increment - if (ar._start_container == parent and ar._start_offset == node_index + 1) { - ar._start_offset += 1; - } - // Step 7e: ranges on parent with end == node_index + 1 increment - if (ar._end_container == parent and ar._end_offset == node_index + 1) { - ar._end_offset += 1; - } + ar.updateForSplitText(target, new_node, offset, parent, node_index); } } @@ -2965,13 +2931,7 @@ pub fn updateRangesForNodeInsertion(self: *Page, parent: *Node, child_index: u32 var it: ?*std.DoublyLinkedList.Node = self._live_ranges.first; while (it) |link| : (it = link.next) { const ar: *AbstractRange = @fieldParentPtr("_range_link", link); - - if (ar._start_container == parent and ar._start_offset > child_index) { - ar._start_offset += 1; - } - if (ar._end_container == parent and ar._end_offset > child_index) { - ar._end_offset += 1; - } + ar.updateForNodeInsertion(parent, child_index); } } @@ -2981,37 +2941,10 @@ pub fn updateRangesForNodeRemoval(self: *Page, parent: *Node, child: *Node, chil var it: ?*std.DoublyLinkedList.Node = self._live_ranges.first; while (it) |link| : (it = link.next) { const ar: *AbstractRange = @fieldParentPtr("_range_link", link); - - // Steps 4-5: ranges whose start/end is an inclusive descendant of child - // get moved to (parent, child_index). - if (isInclusiveDescendantOf(ar._start_container, child)) { - ar._start_container = parent; - ar._start_offset = child_index; - } - if (isInclusiveDescendantOf(ar._end_container, child)) { - ar._end_container = parent; - ar._end_offset = child_index; - } - - // Steps 6-7: ranges on parent at offsets > child_index get decremented. - if (ar._start_container == parent and ar._start_offset > child_index) { - ar._start_offset -= 1; - } - if (ar._end_container == parent and ar._end_offset > child_index) { - ar._end_offset -= 1; - } + ar.updateForNodeRemoval(parent, child, child_index); } } -fn isInclusiveDescendantOf(node: *Node, potential_ancestor: *Node) bool { - var current: ?*Node = node; - while (current) |n| { - if (n == potential_ancestor) return true; - current = n.parentNode(); - } - return false; -} - // TODO: optimize and cleanup, this is called a lot (e.g., innerHTML = '') pub fn parseHtmlAsChildren(self: *Page, node: *Node, html: []const u8) !void { const previous_parse_mode = self._parse_mode; diff --git a/src/browser/webapi/AbstractRange.zig b/src/browser/webapi/AbstractRange.zig index 231c635d..e766ac29 100644 --- a/src/browser/webapi/AbstractRange.zig +++ b/src/browser/webapi/AbstractRange.zig @@ -218,6 +218,91 @@ fn isInclusiveAncestorOf(potential_ancestor: *Node, node: *Node) bool { return isAncestorOf(potential_ancestor, node); } +/// Update this range's boundaries after a replaceData mutation on target. +/// All parameters are in UTF-16 code unit offsets. +pub fn updateForCharacterDataReplace(self: *AbstractRange, target: *Node, offset: u32, count: u32, data_len: u32) void { + if (self._start_container == target) { + if (self._start_offset > offset and self._start_offset <= offset + count) { + self._start_offset = offset; + } else if (self._start_offset > offset + count) { + // Use i64 intermediate to avoid u32 underflow when count > data_len + self._start_offset = @intCast(@as(i64, self._start_offset) + @as(i64, data_len) - @as(i64, count)); + } + } + + if (self._end_container == target) { + if (self._end_offset > offset and self._end_offset <= offset + count) { + self._end_offset = offset; + } else if (self._end_offset > offset + count) { + self._end_offset = @intCast(@as(i64, self._end_offset) + @as(i64, data_len) - @as(i64, count)); + } + } +} + +/// Update this range's boundaries after a splitText operation. +/// Steps 7b-7e of the DOM spec splitText algorithm. +pub fn updateForSplitText(self: *AbstractRange, target: *Node, new_node: *Node, offset: u32, parent: *Node, node_index: u32) void { + // Step 7b: ranges on the original node with start > offset move to new node + if (self._start_container == target and self._start_offset > offset) { + self._start_container = new_node; + self._start_offset = self._start_offset - offset; + } + // Step 7c: ranges on the original node with end > offset move to new node + if (self._end_container == target and self._end_offset > offset) { + self._end_container = new_node; + self._end_offset = self._end_offset - offset; + } + // Step 7d: ranges on parent with start == node_index + 1 increment + if (self._start_container == parent and self._start_offset == node_index + 1) { + self._start_offset += 1; + } + // Step 7e: ranges on parent with end == node_index + 1 increment + if (self._end_container == parent and self._end_offset == node_index + 1) { + self._end_offset += 1; + } +} + +/// Update this range's boundaries after a node insertion. +pub fn updateForNodeInsertion(self: *AbstractRange, parent: *Node, child_index: u32) void { + if (self._start_container == parent and self._start_offset > child_index) { + self._start_offset += 1; + } + if (self._end_container == parent and self._end_offset > child_index) { + self._end_offset += 1; + } +} + +/// Update this range's boundaries after a node removal. +pub fn updateForNodeRemoval(self: *AbstractRange, parent: *Node, child: *Node, child_index: u32) void { + // Steps 4-5: ranges whose start/end is an inclusive descendant of child + // get moved to (parent, child_index). + if (isInclusiveDescendantOf(self._start_container, child)) { + self._start_container = parent; + self._start_offset = child_index; + } + if (isInclusiveDescendantOf(self._end_container, child)) { + self._end_container = parent; + self._end_offset = child_index; + } + + // Steps 6-7: ranges on parent at offsets > child_index get decremented. + if (self._start_container == parent and self._start_offset > child_index) { + self._start_offset -= 1; + } + if (self._end_container == parent and self._end_offset > child_index) { + self._end_offset -= 1; + } +} + +fn isInclusiveDescendantOf(node: *Node, potential_ancestor: *Node) bool { + var current: ?*Node = node; + while (current) |n| { + if (n == potential_ancestor) return true; + current = n.parentNode(); + } + return false; +} + pub const JsApi = struct { pub const bridge = js.Bridge(AbstractRange); diff --git a/src/browser/webapi/Range.zig b/src/browser/webapi/Range.zig index a5789a64..66ce1c93 100644 --- a/src/browser/webapi/Range.zig +++ b/src/browser/webapi/Range.zig @@ -358,7 +358,7 @@ pub fn insertNode(self: *Range, node: *Node, page: *Page) !void { // Per spec step 11: if range was collapsed, extend end to include inserted node. // Non-collapsed ranges are already handled by the live range update in the insert path. - if (was_collapsed and self._proto._start_container == self._proto._end_container) { + if (was_collapsed) { self._proto._end_offset = self._proto._start_offset + 1; } }