Merge pull request #1352 from lightpanda-io/mutation_character_data

Mutation character data
This commit is contained in:
Pierre Tachoire
2026-01-12 15:13:32 +01:00
committed by GitHub
4 changed files with 219 additions and 5 deletions

View File

@@ -820,3 +820,137 @@
}); });
} }
</script> </script>
<script id=deleteContents_crossNode>
{
// Test deleteContents across multiple sibling text nodes
const p = document.createElement('p');
p.appendChild(document.createTextNode('AAAA'));
p.appendChild(document.createTextNode('BBBB'));
p.appendChild(document.createTextNode('CCCC'));
testing.expectEqual(3, p.childNodes.length);
testing.expectEqual('AAAABBBBCCCC', p.textContent);
const range = document.createRange();
// Start at position 2 in first text node ("AA|AA")
range.setStart(p.childNodes[0], 2);
// End at position 2 in third text node ("CC|CC")
range.setEnd(p.childNodes[2], 2);
range.deleteContents();
// Should have truncated first node to "AA" and third node to "CC"
// Middle node should be removed
testing.expectEqual(2, p.childNodes.length);
testing.expectEqual('AA', p.childNodes[0].textContent);
testing.expectEqual('CC', p.childNodes[1].textContent);
testing.expectEqual('AACC', p.textContent);
}
</script>
<script id=deleteContents_crossNode_partial>
{
// Test deleteContents where start node is completely preserved
const p = document.createElement('p');
p.appendChild(document.createTextNode('KEEP'));
p.appendChild(document.createTextNode('DELETE'));
p.appendChild(document.createTextNode('PARTIAL'));
const range = document.createRange();
// Start at end of first text node
range.setStart(p.childNodes[0], 4);
// End in middle of third text node
range.setEnd(p.childNodes[2], 4);
range.deleteContents();
testing.expectEqual(2, p.childNodes.length);
testing.expectEqual('KEEP', p.childNodes[0].textContent);
testing.expectEqual('IAL', p.childNodes[1].textContent);
testing.expectEqual('KEEPIAL', p.textContent);
}
</script>
<script id=extractContents_crossNode>
{
// Test extractContents across multiple sibling text nodes
const p = document.createElement('p');
p.appendChild(document.createTextNode('AAAA'));
p.appendChild(document.createTextNode('BBBB'));
p.appendChild(document.createTextNode('CCCC'));
const range = document.createRange();
range.setStart(p.childNodes[0], 2);
range.setEnd(p.childNodes[2], 2);
const fragment = range.extractContents();
// Original should be truncated
testing.expectEqual(2, p.childNodes.length);
testing.expectEqual('AA', p.childNodes[0].textContent);
testing.expectEqual('CC', p.childNodes[1].textContent);
// Fragment should contain extracted content
testing.expectEqual(3, fragment.childNodes.length);
testing.expectEqual('AA', fragment.childNodes[0].textContent);
testing.expectEqual('BBBB', fragment.childNodes[1].textContent);
testing.expectEqual('CC', fragment.childNodes[2].textContent);
}
</script>
<script id=cloneContents_crossNode>
{
// Test cloneContents across multiple sibling text nodes
const p = document.createElement('p');
p.appendChild(document.createTextNode('AAAA'));
p.appendChild(document.createTextNode('BBBB'));
p.appendChild(document.createTextNode('CCCC'));
const range = document.createRange();
range.setStart(p.childNodes[0], 2);
range.setEnd(p.childNodes[2], 2);
const fragment = range.cloneContents();
// Original should be unchanged
testing.expectEqual(3, p.childNodes.length);
testing.expectEqual('AAAA', p.childNodes[0].textContent);
testing.expectEqual('BBBB', p.childNodes[1].textContent);
testing.expectEqual('CCCC', p.childNodes[2].textContent);
// Fragment should contain cloned content
testing.expectEqual(3, fragment.childNodes.length);
testing.expectEqual('AA', fragment.childNodes[0].textContent);
testing.expectEqual('BBBB', fragment.childNodes[1].textContent);
testing.expectEqual('CC', fragment.childNodes[2].textContent);
}
</script>
<script id=deleteContents_crossNode_withElements>
{
// Test deleteContents with mixed text and element nodes
const div = document.createElement('div');
div.appendChild(document.createTextNode('Start'));
const span = document.createElement('span');
span.textContent = 'Middle';
div.appendChild(span);
div.appendChild(document.createTextNode('End'));
testing.expectEqual(3, div.childNodes.length);
const range = document.createRange();
// Start in middle of first text node
range.setStart(div.childNodes[0], 2);
// End in middle of last text node
range.setEnd(div.childNodes[2], 1);
range.deleteContents();
// Should keep "St" from start, remove span, keep "nd" from end
testing.expectEqual(2, div.childNodes.length);
testing.expectEqual('St', div.childNodes[0].textContent);
testing.expectEqual('nd', div.childNodes[1].textContent);
testing.expectEqual('Stnd', div.textContent);
}
</script>

View File

@@ -69,6 +69,10 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
copied_options.attributeFilter = filter_copy; copied_options.attributeFilter = filter_copy;
} }
if (options.characterDataOldValue) {
copied_options.characterData = true;
}
// Check if already observing this target // Check if already observing this target
for (self._observing.items) |*obs| { for (self._observing.items) |*obs| {
if (obs.target == target) { if (obs.target == target) {
@@ -274,6 +278,13 @@ pub const MutationRecord = struct {
return self._target; return self._target;
} }
pub fn getAttributeNamespace(self: *const MutationRecord) ?[]const u8 {
if (self._attribute_name != null) {
return "http://www.w3.org/1999/xhtml";
}
return null;
}
pub fn getAttributeName(self: *const MutationRecord) ?[]const u8 { pub fn getAttributeName(self: *const MutationRecord) ?[]const u8 {
return self._attribute_name; return self._attribute_name;
} }
@@ -310,6 +321,7 @@ pub const MutationRecord = struct {
pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{}); pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{});
pub const target = bridge.accessor(MutationRecord.getTarget, null, .{}); pub const target = bridge.accessor(MutationRecord.getTarget, null, .{});
pub const attributeName = bridge.accessor(MutationRecord.getAttributeName, null, .{}); pub const attributeName = bridge.accessor(MutationRecord.getAttributeName, null, .{});
pub const attributeNamespace = bridge.accessor(MutationRecord.getAttributeNamespace, null, .{});
pub const oldValue = bridge.accessor(MutationRecord.getOldValue, null, .{}); pub const oldValue = bridge.accessor(MutationRecord.getOldValue, null, .{});
pub const addedNodes = bridge.accessor(MutationRecord.getAddedNodes, null, .{}); pub const addedNodes = bridge.accessor(MutationRecord.getAddedNodes, null, .{});
pub const removedNodes = bridge.accessor(MutationRecord.getRemovedNodes, null, .{}); pub const removedNodes = bridge.accessor(MutationRecord.getRemovedNodes, null, .{});

View File

@@ -652,9 +652,9 @@ pub fn getData(self: *const Node) []const u8 {
}; };
} }
pub fn setData(self: *Node, data: []const u8) void { pub fn setData(self: *Node, data: []const u8, page: *Page) !void {
switch (self._type) { switch (self._type) {
.cdata => |c| c._data = data, .cdata => |c| try c.setData(data, page),
else => {}, else => {},
} }
} }

View File

@@ -357,7 +357,7 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
u8, u8,
&.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] }, &.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] },
); );
self._proto._start_container.setData(new_text); try self._proto._start_container.setData(new_text, page);
} else { } else {
// Delete child nodes in range // Delete child nodes in range
var offset = self._proto._start_offset; var offset = self._proto._start_offset;
@@ -371,8 +371,43 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
return; return;
} }
// Complex case: different containers - simplified implementation // Complex case: different containers
// Just collapse the range for now // Handle start container - if it's a text node, truncate it
if (self._proto._start_container.is(Node.CData)) |_| {
const text_data = self._proto._start_container.getData();
if (self._proto._start_offset < text_data.len) {
// Keep only the part before start_offset
const new_text = text_data[0..self._proto._start_offset];
try self._proto._start_container.setData(new_text, page);
}
}
// Handle end container - if it's a text node, truncate it
if (self._proto._end_container.is(Node.CData)) |_| {
const text_data = self._proto._end_container.getData();
if (self._proto._end_offset < text_data.len) {
// Keep only the part from end_offset onwards
const new_text = text_data[self._proto._end_offset..];
try self._proto._end_container.setData(new_text, page);
} else if (self._proto._end_offset == text_data.len) {
// If we're at the end, set to empty (will be removed if needed)
try self._proto._end_container.setData("", page);
}
}
// 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.?, page);
}
current = next;
}
}
self.collapse(true); self.collapse(true);
} }
@@ -401,6 +436,39 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
} }
} }
} }
} 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();
if (self._proto._start_offset < text_data.len) {
// Clone from start_offset to end of text
const cloned_text = text_data[self._proto._start_offset..];
const text_node = try page.createTextNode(cloned_text);
_ = try fragment.asNode().appendChild(text_node, page);
}
}
// 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 cloned = try current.?.cloneNode(true, page);
_ = try fragment.asNode().appendChild(cloned, page);
current = current.?.nextSibling();
}
}
// Clone partial end container
if (self._proto._end_container.is(Node.CData)) |_| {
const text_data = self._proto._end_container.getData();
if (self._proto._end_offset > 0 and self._proto._end_offset <= text_data.len) {
// Clone from start to end_offset
const cloned_text = text_data[0..self._proto._end_offset];
const text_node = try page.createTextNode(cloned_text);
_ = try fragment.asNode().appendChild(text_node, page);
}
}
} }
return fragment; return fragment;