address review: move per-range logic to AbstractRange, simplify collapsed check

Move the per-range update logic from Page into AbstractRange methods
(updateForCharacterDataReplace, updateForSplitText, updateForNodeInsertion,
updateForNodeRemoval). Page now just iterates the list and delegates.

Remove redundant start_container == end_container check in insertNode —
collapsed already implies same container.
This commit is contained in:
egrs
2026-03-11 07:26:20 +01:00
parent 7927ad8fcf
commit d2c55da6c9
3 changed files with 90 additions and 72 deletions

View File

@@ -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,35 +2941,8 @@ 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;
ar.updateForNodeRemoval(parent, child, 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;
}
}
}
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 = '')

View File

@@ -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);

View File

@@ -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;
}
}