mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-01 18:26:06 -04:00
Merge pull request #2957 from lightpanda-io/dom-pr-16-range-clone-extract
webapi: Range cloneContents/extractContents; replaceChild live-range ordering
This commit is contained in:
@@ -830,11 +830,26 @@ pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, frame: *Fra
|
||||
const notify = Frame.observers.hasMutationObservers(frame);
|
||||
|
||||
if (new_child == old_child) {
|
||||
// Replacing a node with itself doesn't change the tree; observers
|
||||
// still get a removal record followed by an addition record.
|
||||
// Replacing a node with itself leaves the tree unchanged, but the
|
||||
// remove-then-reinsert still happens: live ranges anchored inside the
|
||||
// node move to its old position, and observers get a removal record
|
||||
// followed by an addition record.
|
||||
const prev = old_child.previousSibling();
|
||||
const next = old_child.nextSibling();
|
||||
const was_connected = old_child.isConnected();
|
||||
frame.removeNode(self, old_child, .{ .will_be_reconnected = was_connected, .notify_observers = false });
|
||||
if (next) |reference| {
|
||||
try frame.insertNodeRelative(self, old_child, .{ .before = reference }, .{
|
||||
.child_already_connected = was_connected,
|
||||
.notify_observers = false,
|
||||
});
|
||||
} else {
|
||||
try frame.appendNode(self, old_child, .{
|
||||
.child_already_connected = was_connected,
|
||||
.notify_observers = false,
|
||||
});
|
||||
}
|
||||
if (notify) {
|
||||
const prev = old_child.previousSibling();
|
||||
const next = old_child.nextSibling();
|
||||
const nodes = [_]*Node{old_child};
|
||||
Frame.observers.notifyChildListChange(frame, self, &.{}, &nodes, prev, next);
|
||||
Frame.observers.notifyChildListChange(frame, self, &nodes, &.{}, prev, next);
|
||||
@@ -842,20 +857,17 @@ pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, frame: *Fra
|
||||
return old_child;
|
||||
}
|
||||
|
||||
// The combined record's siblings are captured before new_child is
|
||||
// removed from its old position (spec: referenceChild and
|
||||
// previousSibling are set before the adopt step), so new_child itself
|
||||
// can be the record's previousSibling. A referenceChild that is
|
||||
// new_child becomes new_child's next sibling.
|
||||
// Spec order: capture the reference points, remove new_child from its
|
||||
// current position (notifying normally - internal replacement), remove
|
||||
// old_child (suppressed), insert new_child before the reference
|
||||
// (suppressed), and queue one combined record. Removing old_child before
|
||||
// inserting matters for live ranges anchored on this parent's offsets.
|
||||
const prev = old_child.previousSibling();
|
||||
var next = old_child.nextSibling();
|
||||
if (next == new_child) {
|
||||
next = new_child.nextSibling();
|
||||
var reference = old_child.nextSibling();
|
||||
if (reference == new_child) {
|
||||
reference = new_child.nextSibling();
|
||||
}
|
||||
|
||||
// Removing new_child from its current position (internal replacement)
|
||||
// notifies normally; the replacement itself queues one combined record
|
||||
// with both the added and the removed node.
|
||||
const child_already_connected = new_child.isConnected();
|
||||
const child_owner = new_child.ownerDocument(frame);
|
||||
const parent_owner = self.ownerDocument(frame) orelse self.as(Document);
|
||||
@@ -872,35 +884,43 @@ pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, frame: *Fra
|
||||
}
|
||||
|
||||
var added: std.ArrayList(*Node) = .empty;
|
||||
if (new_child.is(DocumentFragment)) |_| {
|
||||
if (notify) {
|
||||
if (notify) {
|
||||
if (new_child.is(DocumentFragment)) |_| {
|
||||
var it = new_child.childrenIterator();
|
||||
while (it.next()) |fragment_child| {
|
||||
try added.append(frame.call_arena, fragment_child);
|
||||
}
|
||||
}
|
||||
try frame.moveAllChildren(new_child, self, old_child, .silent_parent);
|
||||
} else {
|
||||
if (notify) {
|
||||
} else {
|
||||
try added.append(frame.call_arena, new_child);
|
||||
}
|
||||
}
|
||||
|
||||
frame.removeNode(self, old_child, .{ .will_be_reconnected = false, .notify_observers = false });
|
||||
|
||||
if (new_child.is(DocumentFragment)) |_| {
|
||||
try frame.moveAllChildren(new_child, self, reference, .silent_parent);
|
||||
} else if (reference) |ref| {
|
||||
try frame.insertNodeRelative(
|
||||
self,
|
||||
new_child,
|
||||
.{ .before = old_child },
|
||||
.{ .before = ref },
|
||||
.{
|
||||
.child_already_connected = child_already_connected,
|
||||
.adopting_to_new_document = adopting,
|
||||
.notify_observers = false,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
try frame.appendNode(self, new_child, .{
|
||||
.child_already_connected = child_already_connected,
|
||||
.adopting_to_new_document = adopting,
|
||||
.notify_observers = false,
|
||||
});
|
||||
}
|
||||
|
||||
frame.removeNode(self, old_child, .{ .will_be_reconnected = false, .notify_observers = false });
|
||||
|
||||
if (notify) {
|
||||
const removed = [_]*Node{old_child};
|
||||
Frame.observers.notifyChildListChange(frame, self, added.items, &removed, prev, next);
|
||||
Frame.observers.notifyChildListChange(frame, self, added.items, &removed, prev, reference);
|
||||
}
|
||||
|
||||
return old_child;
|
||||
|
||||
@@ -470,97 +470,204 @@ fn collectContained(self: *const Range, node: *Node, list: *std.ArrayList(*Node)
|
||||
}
|
||||
|
||||
fn nodeContained(self: *const Range, node: *Node) bool {
|
||||
const after_start = AbstractRange.compareBoundaryPoints(
|
||||
return containedBetween(
|
||||
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 {
|
||||
const fragment = try DocumentFragment.init(frame);
|
||||
|
||||
if (self._proto.getCollapsed()) return fragment;
|
||||
|
||||
// Simple case: same container
|
||||
if (self._proto._start_container == self._proto._end_container) {
|
||||
if (self._proto._start_container.is(Node.CData)) |_| {
|
||||
// Clone part of text node
|
||||
const text_data = self._proto._start_container.getData().str();
|
||||
const byte_start = byteOffset(text_data, self._proto._start_offset);
|
||||
const byte_end = byteOffset(text_data, self._proto._end_offset);
|
||||
if (byte_start < text_data.len and byte_end <= text_data.len) {
|
||||
const cloned_text = text_data[byte_start..byte_end];
|
||||
const text_node = try Frame.node_factory.createTextNode(frame, cloned_text);
|
||||
_ = try fragment.asNode().appendChild(text_node, frame);
|
||||
}
|
||||
} else {
|
||||
// Clone child nodes in range
|
||||
var offset = self._proto._start_offset;
|
||||
while (offset < self._proto._end_offset) : (offset += 1) {
|
||||
if (self._proto._start_container.getChildAt(offset)) |child| {
|
||||
if (try child.cloneNodeForAppending(true, frame)) |cloned| {
|
||||
_ = try fragment.asNode().appendChild(cloned, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Complex case: different containers
|
||||
// Clone partial start container
|
||||
if (self._proto._start_container.is(Node.CData)) |_| {
|
||||
const text_data = self._proto._start_container.getData().str();
|
||||
const byte_start = byteOffset(text_data, self._proto._start_offset);
|
||||
if (byte_start < text_data.len) {
|
||||
// Clone from start_offset to end of text
|
||||
const cloned_text = text_data[byte_start..];
|
||||
const text_node = try Frame.node_factory.createTextNode(frame, cloned_text);
|
||||
_ = try fragment.asNode().appendChild(text_node, frame);
|
||||
}
|
||||
}
|
||||
|
||||
// Clone nodes between start and end containers (siblings case)
|
||||
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 (try current.?.cloneNodeForAppending(true, frame)) |cloned| {
|
||||
_ = try fragment.asNode().appendChild(cloned, frame);
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
// Clone partial end container
|
||||
if (self._proto._end_container.is(Node.CData)) |_| {
|
||||
const text_data = self._proto._end_container.getData().str();
|
||||
const byte_end = byteOffset(text_data, self._proto._end_offset);
|
||||
if (byte_end > 0 and byte_end <= text_data.len) {
|
||||
// Clone from start to end_offset
|
||||
const cloned_text = text_data[0..byte_end];
|
||||
const text_node = try Frame.node_factory.createTextNode(frame, cloned_text);
|
||||
_ = try fragment.asNode().appendChild(text_node, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try cloneContentsBetween(
|
||||
frame,
|
||||
fragment.asNode(),
|
||||
self._proto._start_container,
|
||||
self._proto._start_offset,
|
||||
self._proto._end_container,
|
||||
self._proto._end_offset,
|
||||
);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
// The DOM "clone the contents of a range" algorithm, on explicit boundary
|
||||
// points so the partially-contained recursion doesn't need Range objects.
|
||||
fn cloneContentsBetween(frame: *Frame, out: *Node, start_node: *Node, start_offset: u32, end_node: *Node, end_offset: u32) !void {
|
||||
if (start_node == end_node) {
|
||||
if (start_node.is(Node.CData)) |cdata| {
|
||||
const data = cdata.getData().str();
|
||||
const cloned = (try start_node.cloneNodeForAppending(false, frame)) orelse return;
|
||||
try cloned.setData(data[byteOffset(data, start_offset)..byteOffset(data, end_offset)], frame);
|
||||
_ = try out.appendChild(cloned, frame);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The closest common ancestor of the two boundary points.
|
||||
var common = start_node;
|
||||
while (common != end_node and !common.contains(end_node)) {
|
||||
common = common.parentNode() orelse break;
|
||||
}
|
||||
|
||||
var child = common.firstChild();
|
||||
while (child) |c| : (child = c.nextSibling()) {
|
||||
const contains_start = c == start_node or c.contains(start_node);
|
||||
const contains_end = c == end_node or c.contains(end_node);
|
||||
|
||||
if (contains_start) {
|
||||
// First partially contained child.
|
||||
if (c.is(Node.CData)) |cdata| {
|
||||
// c is the start node itself.
|
||||
const data = cdata.getData().str();
|
||||
const cloned = (try c.cloneNodeForAppending(false, frame)) orelse continue;
|
||||
try cloned.setData(data[byteOffset(data, start_offset)..], frame);
|
||||
_ = try out.appendChild(cloned, frame);
|
||||
} else {
|
||||
const cloned = (try c.cloneNodeForAppending(false, frame)) orelse continue;
|
||||
_ = try out.appendChild(cloned, frame);
|
||||
try cloneContentsBetween(frame, cloned, start_node, start_offset, c, c.getLength());
|
||||
}
|
||||
} else if (contains_end) {
|
||||
// Last partially contained child.
|
||||
if (c.is(Node.CData)) |cdata| {
|
||||
// c is the end node itself.
|
||||
const data = cdata.getData().str();
|
||||
const cloned = (try c.cloneNodeForAppending(false, frame)) orelse continue;
|
||||
try cloned.setData(data[0..byteOffset(data, end_offset)], frame);
|
||||
_ = try out.appendChild(cloned, frame);
|
||||
} else {
|
||||
const cloned = (try c.cloneNodeForAppending(false, frame)) orelse continue;
|
||||
_ = try out.appendChild(cloned, frame);
|
||||
try cloneContentsBetween(frame, cloned, c, 0, end_node, end_offset);
|
||||
}
|
||||
} else if (containedBetween(c, start_node, start_offset, end_node, end_offset)) {
|
||||
if (c._type == .document_type) {
|
||||
return error.HierarchyError;
|
||||
}
|
||||
const cloned = (try c.cloneNodeForAppending(true, frame)) orelse continue;
|
||||
_ = try out.appendChild(cloned, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn containedBetween(node: *Node, start_node: *Node, start_offset: u32, end_node: *Node, end_offset: u32) bool {
|
||||
const after_start = AbstractRange.compareBoundaryPoints(node, 0, start_node, start_offset) == .after;
|
||||
if (!after_start) return false;
|
||||
return AbstractRange.compareBoundaryPoints(node, node.getLength(), end_node, end_offset) == .before;
|
||||
}
|
||||
|
||||
pub fn extractContents(self: *Range, frame: *Frame) !*DocumentFragment {
|
||||
const fragment = try self.cloneContents(frame);
|
||||
try self.deleteContents(frame);
|
||||
const fragment = try DocumentFragment.init(frame);
|
||||
if (self._proto.getCollapsed()) return fragment;
|
||||
|
||||
frame.domChanged();
|
||||
|
||||
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;
|
||||
|
||||
// Where the range collapses to afterwards; same rule as deleteContents.
|
||||
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;
|
||||
}
|
||||
|
||||
try extractContentsBetween(frame, fragment.asNode(), start_node, start_offset, end_node, end_offset);
|
||||
|
||||
try self.setStart(new_node, new_offset);
|
||||
try self.setEnd(new_node, new_offset);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
// The DOM "extract" algorithm: contained nodes MOVE into the output
|
||||
// (preserving identity); only the partially contained boundary
|
||||
// CharacterData nodes and the partially contained element shells are cloned.
|
||||
// Children are classified before anything moves, since moving a contained
|
||||
// child shifts the indices the boundary comparisons rely on.
|
||||
fn extractContentsBetween(frame: *Frame, out: *Node, start_node: *Node, start_offset: u32, end_node: *Node, end_offset: u32) !void {
|
||||
if (start_node == end_node) {
|
||||
if (start_node.is(Node.CData)) |cdata| {
|
||||
const data = cdata.getData().str();
|
||||
const cloned = (try start_node.cloneNodeForAppending(false, frame)) orelse return;
|
||||
try cloned.setData(data[byteOffset(data, start_offset)..byteOffset(data, end_offset)], frame);
|
||||
_ = try out.appendChild(cloned, frame);
|
||||
try cdata.replaceData(start_offset, end_offset - start_offset, "", frame);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var common = start_node;
|
||||
while (common != end_node and !common.contains(end_node)) {
|
||||
common = common.parentNode() orelse break;
|
||||
}
|
||||
|
||||
var first_partial: ?*Node = null;
|
||||
var last_partial: ?*Node = null;
|
||||
var contained: std.ArrayList(*Node) = .empty;
|
||||
|
||||
var child = common.firstChild();
|
||||
while (child) |c| : (child = c.nextSibling()) {
|
||||
if (c == start_node or c.contains(start_node)) {
|
||||
first_partial = c;
|
||||
} else if (c == end_node or c.contains(end_node)) {
|
||||
last_partial = c;
|
||||
} else if (containedBetween(c, start_node, start_offset, end_node, end_offset)) {
|
||||
if (c._type == .document_type) {
|
||||
return error.HierarchyError;
|
||||
}
|
||||
try contained.append(frame.call_arena, c);
|
||||
}
|
||||
}
|
||||
|
||||
if (first_partial) |c| {
|
||||
if (c.is(Node.CData)) |cdata| {
|
||||
// c is the start node itself.
|
||||
const data = cdata.getData().str();
|
||||
const byte_start = byteOffset(data, start_offset);
|
||||
const cloned = (try c.cloneNodeForAppending(false, frame)) orelse return;
|
||||
try cloned.setData(data[byte_start..], frame);
|
||||
_ = try out.appendChild(cloned, frame);
|
||||
const length: u32 = @intCast(cdata.getLength());
|
||||
try cdata.replaceData(start_offset, length - start_offset, "", frame);
|
||||
} else {
|
||||
const cloned = (try c.cloneNodeForAppending(false, frame)) orelse return;
|
||||
_ = try out.appendChild(cloned, frame);
|
||||
try extractContentsBetween(frame, cloned, start_node, start_offset, c, c.getLength());
|
||||
}
|
||||
}
|
||||
|
||||
for (contained.items) |c| {
|
||||
_ = try out.appendChild(c, frame);
|
||||
}
|
||||
|
||||
if (last_partial) |c| {
|
||||
if (c.is(Node.CData)) |cdata| {
|
||||
// c is the end node itself.
|
||||
const data = cdata.getData().str();
|
||||
const cloned = (try c.cloneNodeForAppending(false, frame)) orelse return;
|
||||
try cloned.setData(data[0..byteOffset(data, end_offset)], frame);
|
||||
_ = try out.appendChild(cloned, frame);
|
||||
try cdata.replaceData(0, end_offset, "", frame);
|
||||
} else {
|
||||
const cloned = (try c.cloneNodeForAppending(false, frame)) orelse return;
|
||||
_ = try out.appendChild(cloned, frame);
|
||||
try extractContentsBetween(frame, cloned, c, 0, end_node, end_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn surroundContents(self: *Range, new_parent: *Node, frame: *Frame) !void {
|
||||
// Extract contents
|
||||
const contents = try self.extractContents(frame);
|
||||
|
||||
Reference in New Issue
Block a user