mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 01:36:15 -04:00
webapi: implement the spec algorithm for Range.deleteContents
Fixes the 20 failing subtests of WPT /dom/ranges/Range-deleteContents.html (105/125 -> 125/125) and 12 of Range-extractContents.html (which delegates its deletion half): deleteContents only handled same-container ranges and the "boundary containers are siblings" case, and always collapsed to the start point. It now follows the DOM algorithm: - same-CharacterData ranges replace the data in place (replaceData, so live ranges update); - the top-most contained nodes (contained per boundary-point comparison, with no contained parent) are collected before any mutation and removed wherever they live in the tree, not just among siblings; - partially contained CharacterData boundaries are truncated with replaceData; - the range collapses to the spec's "new node/new offset": the start point when the start node is an inclusive ancestor of the end node, otherwise just after the highest partially-contained ancestor of the start node. Coverage: - /dom/ranges/Range-deleteContents.html 105/125 -> 125/125 (fully green) - /dom/ranges/Range-extractContents.html 141/187 -> 153/187 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -391,76 +391,87 @@ pub fn deleteContents(self: *Range, frame: *Frame) !void {
|
||||
}
|
||||
frame.domChanged();
|
||||
|
||||
// Simple case: same container
|
||||
if (self._proto._start_container == self._proto._end_container) {
|
||||
if (self._proto._start_container.is(Node.CData)) |cdata| {
|
||||
// Delete part of text node
|
||||
const old_value = cdata.getData();
|
||||
const text_data = old_value.str();
|
||||
cdata._data = try String.concat(
|
||||
frame.arena,
|
||||
&.{
|
||||
text_data[0..byteOffset(text_data, self._proto._start_offset)],
|
||||
text_data[byteOffset(text_data, self._proto._end_offset)..],
|
||||
},
|
||||
);
|
||||
Frame.observers.notifyCharacterDataChange(frame, self._proto._start_container, old_value);
|
||||
const start_node = self._proto._start_container;
|
||||
const start_offset = self._proto._start_offset;
|
||||
const end_node = self._proto._end_container;
|
||||
const end_offset = self._proto._end_offset;
|
||||
|
||||
// Same CharacterData container: replace the data in place.
|
||||
if (start_node == end_node) {
|
||||
if (start_node.is(Node.CData)) |cdata| {
|
||||
try cdata.replaceData(start_offset, end_offset - start_offset, "", frame);
|
||||
try self.setStart(start_node, start_offset);
|
||||
try self.setEnd(start_node, start_offset);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Contained nodes whose parent isn't also contained, in tree order.
|
||||
var to_remove: std.ArrayList(*Node) = .empty;
|
||||
try self.collectContained(self._proto.getCommonAncestorContainer(), &to_remove, frame.call_arena);
|
||||
|
||||
// Where the collapsed range ends up: the start point if the start node
|
||||
// is an inclusive ancestor of the end node; otherwise just after the
|
||||
// highest partially-contained ancestor of the start node.
|
||||
var new_node = start_node;
|
||||
var new_offset = start_offset;
|
||||
if (start_node != end_node and !start_node.contains(end_node)) {
|
||||
var reference = start_node;
|
||||
while (reference.parentNode()) |parent| {
|
||||
if (parent == end_node or parent.contains(end_node)) break;
|
||||
reference = parent;
|
||||
}
|
||||
new_node = reference.parentNode().?;
|
||||
new_offset = (new_node.getChildIndex(reference) orelse 0) + 1;
|
||||
}
|
||||
|
||||
if (start_node.is(Node.CData)) |cdata| {
|
||||
const length: u32 = @intCast(cdata.getLength());
|
||||
try cdata.replaceData(start_offset, length - start_offset, "", frame);
|
||||
}
|
||||
|
||||
for (to_remove.items) |node| {
|
||||
if (node.parentNode()) |parent| {
|
||||
_ = try parent.removeChild(node, frame);
|
||||
}
|
||||
}
|
||||
|
||||
if (end_node.is(Node.CData)) |cdata| {
|
||||
try cdata.replaceData(0, end_offset, "", frame);
|
||||
}
|
||||
|
||||
try self.setStart(new_node, new_offset);
|
||||
try self.setEnd(new_node, new_offset);
|
||||
}
|
||||
|
||||
// A node is contained in the range when its whole extent lies between the
|
||||
// range's boundary points. Appends the top-most contained nodes (those whose
|
||||
// parent isn't contained) under `node`, without descending into them.
|
||||
fn collectContained(self: *const Range, node: *Node, list: *std.ArrayList(*Node), arena: std.mem.Allocator) !void {
|
||||
var child = node.firstChild();
|
||||
while (child) |c| : (child = c.nextSibling()) {
|
||||
if (self.nodeContained(c)) {
|
||||
try list.append(arena, c);
|
||||
} else {
|
||||
// Delete child nodes in range.
|
||||
// Capture count before the loop: removeChild triggers live range
|
||||
// updates that decrement _end_offset on each removal.
|
||||
const count = self._proto._end_offset - self._proto._start_offset;
|
||||
var i: u32 = 0;
|
||||
while (i < count) : (i += 1) {
|
||||
if (self._proto._start_container.getChildAt(self._proto._start_offset)) |child| {
|
||||
_ = try self._proto._start_container.removeChild(child, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.collapse(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Complex case: different containers
|
||||
// Handle start container - if it's a text node, truncate it
|
||||
if (self._proto._start_container.is(Node.CData)) |cdata| {
|
||||
const text_data = cdata._data.str();
|
||||
const byte_start = byteOffset(text_data, self._proto._start_offset);
|
||||
if (byte_start < text_data.len) {
|
||||
// Keep only the part before start_offset
|
||||
const new_text = text_data[0..byte_start];
|
||||
try self._proto._start_container.setData(new_text, frame);
|
||||
try self.collectContained(c, list, arena);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle end container - if it's a text node, truncate it
|
||||
if (self._proto._end_container.is(Node.CData)) |cdata| {
|
||||
const text_data = cdata._data.str();
|
||||
const byte_end = byteOffset(text_data, self._proto._end_offset);
|
||||
if (byte_end < text_data.len) {
|
||||
// Keep only the part from end_offset onwards
|
||||
const new_text = text_data[byte_end..];
|
||||
try self._proto._end_container.setData(new_text, frame);
|
||||
} else if (byte_end == text_data.len) {
|
||||
// If we're at the end, set to empty (will be removed if needed)
|
||||
try self._proto._end_container.setData("", frame);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove nodes between start and end containers
|
||||
// For now, handle the common case where they're siblings
|
||||
if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) {
|
||||
var current = self._proto._start_container.nextSibling();
|
||||
while (current != null and current != self._proto._end_container) {
|
||||
const next = current.?.nextSibling();
|
||||
if (current.?.parentNode()) |parent| {
|
||||
_ = try parent.removeChild(current.?, frame);
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
self.collapse(true);
|
||||
fn nodeContained(self: *const Range, node: *Node) bool {
|
||||
const after_start = AbstractRange.compareBoundaryPoints(
|
||||
node,
|
||||
0,
|
||||
self._proto._start_container,
|
||||
self._proto._start_offset,
|
||||
) == .after;
|
||||
if (!after_start) return false;
|
||||
return AbstractRange.compareBoundaryPoints(
|
||||
node,
|
||||
node.getLength(),
|
||||
self._proto._end_container,
|
||||
self._proto._end_offset,
|
||||
) == .before;
|
||||
}
|
||||
|
||||
pub fn cloneContents(self: *const Range, frame: *Frame) !*DocumentFragment {
|
||||
|
||||
Reference in New Issue
Block a user