mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-15 15:58:57 +00:00
Range
This commit is contained in:
@@ -516,6 +516,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/DOMRect.zig"),
|
||||
@import("../webapi/DOMParser.zig"),
|
||||
@import("../webapi/XMLSerializer.zig"),
|
||||
@import("../webapi/Range.zig"),
|
||||
@import("../webapi/NodeFilter.zig"),
|
||||
@import("../webapi/Element.zig"),
|
||||
@import("../webapi/element/DOMStringMap.zig"),
|
||||
|
||||
@@ -73,7 +73,11 @@
|
||||
testing.expectEqual([0], Array.from(one.keys()));
|
||||
testing.expectEqual([p10], Array.from(one.values()));
|
||||
testing.expectEqual([[0, p10]], Array.from(one.entries()));
|
||||
|
||||
testing.expectEqual([p10], Array.from(one));
|
||||
let foreach = [];
|
||||
one.forEach((p) => foreach.push(p));
|
||||
testing.expectEqual([p10], foreach);
|
||||
</script>
|
||||
|
||||
<script id=contains>
|
||||
|
||||
377
src/browser/tests/range.html
Normal file
377
src/browser/tests/range.html
Normal file
@@ -0,0 +1,377 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="testing.js"></script>
|
||||
<body>
|
||||
<div id="test-content">
|
||||
<p id="p1">First paragraph</p>
|
||||
<p id="p2">Second paragraph</p>
|
||||
<span id="s1">Span content</span>
|
||||
</div>
|
||||
|
||||
<script id=basic>
|
||||
{
|
||||
const range = document.createRange();
|
||||
testing.expectEqual('object', typeof range);
|
||||
testing.expectEqual(true, range instanceof Range);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=initial_state>
|
||||
{
|
||||
const range = document.createRange();
|
||||
|
||||
// New range should be collapsed at document position
|
||||
testing.expectEqual(document, range.startContainer);
|
||||
testing.expectEqual(0, range.startOffset);
|
||||
testing.expectEqual(document, range.endContainer);
|
||||
testing.expectEqual(0, range.endOffset);
|
||||
testing.expectEqual(true, range.collapsed);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=setStart_setEnd>
|
||||
{
|
||||
const range = document.createRange();
|
||||
const p1 = $('#p1');
|
||||
const p2 = $('#p2');
|
||||
|
||||
range.setStart(p1, 0);
|
||||
testing.expectEqual(p1, range.startContainer);
|
||||
testing.expectEqual(0, range.startOffset);
|
||||
// After setStart, if new start is after original end, range collapses
|
||||
// Since original end was at (document, 0) and p1 is inside document,
|
||||
// the range auto-collapses to the new start
|
||||
testing.expectEqual(p1, range.endContainer);
|
||||
testing.expectEqual(0, range.endOffset);
|
||||
testing.expectEqual(true, range.collapsed);
|
||||
|
||||
range.setEnd(p2, 0);
|
||||
testing.expectEqual(p2, range.endContainer);
|
||||
testing.expectEqual(0, range.endOffset);
|
||||
testing.expectEqual(false, range.collapsed);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=collapse>
|
||||
{
|
||||
const range = document.createRange();
|
||||
const p1 = $('#p1');
|
||||
const p2 = $('#p2');
|
||||
|
||||
range.setStart(p1, 0);
|
||||
range.setEnd(p2, 1);
|
||||
testing.expectEqual(false, range.collapsed);
|
||||
|
||||
// Collapse to start
|
||||
range.collapse(true);
|
||||
testing.expectEqual(p1, range.startContainer);
|
||||
testing.expectEqual(p1, range.endContainer);
|
||||
testing.expectEqual(0, range.startOffset);
|
||||
testing.expectEqual(0, range.endOffset);
|
||||
testing.expectEqual(true, range.collapsed);
|
||||
|
||||
// Reset and collapse to end
|
||||
range.setStart(p1, 0);
|
||||
range.setEnd(p2, 1);
|
||||
range.collapse(false);
|
||||
testing.expectEqual(p2, range.startContainer);
|
||||
testing.expectEqual(p2, range.endContainer);
|
||||
testing.expectEqual(1, range.startOffset);
|
||||
testing.expectEqual(1, range.endOffset);
|
||||
testing.expectEqual(true, range.collapsed);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=selectNode>
|
||||
{
|
||||
const range = document.createRange();
|
||||
const p1 = $('#p1');
|
||||
const parent = p1.parentNode;
|
||||
|
||||
range.selectNode(p1);
|
||||
|
||||
testing.expectEqual(parent, range.startContainer);
|
||||
testing.expectEqual(parent, range.endContainer);
|
||||
testing.expectEqual(false, range.collapsed);
|
||||
|
||||
// Start should be before p1, end should be after p1
|
||||
const startOffset = range.startOffset;
|
||||
const endOffset = range.endOffset;
|
||||
testing.expectEqual(startOffset + 1, endOffset);
|
||||
testing.expectEqual(p1, parent.childNodes[startOffset]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=selectNodeContents>
|
||||
{
|
||||
const range = document.createRange();
|
||||
const p1 = $('#p1');
|
||||
|
||||
range.selectNodeContents(p1);
|
||||
|
||||
testing.expectEqual(p1, range.startContainer);
|
||||
testing.expectEqual(p1, range.endContainer);
|
||||
testing.expectEqual(0, range.startOffset);
|
||||
testing.expectEqual(p1.childNodes.length, range.endOffset);
|
||||
testing.expectEqual(false, range.collapsed);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=setStartBefore_setStartAfter>
|
||||
{
|
||||
const range = document.createRange();
|
||||
const p1 = $('#p1');
|
||||
const p2 = $('#p2');
|
||||
const parent = p1.parentNode;
|
||||
|
||||
range.setStartBefore(p1);
|
||||
testing.expectEqual(parent, range.startContainer);
|
||||
const beforeOffset = range.startOffset;
|
||||
testing.expectEqual(p1, parent.childNodes[beforeOffset]);
|
||||
|
||||
range.setStartAfter(p1);
|
||||
testing.expectEqual(parent, range.startContainer);
|
||||
const afterOffset = range.startOffset;
|
||||
testing.expectEqual(afterOffset, beforeOffset + 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=setEndBefore_setEndAfter>
|
||||
{
|
||||
const range = document.createRange();
|
||||
const p2 = $('#p2');
|
||||
const parent = p2.parentNode;
|
||||
|
||||
range.setEndBefore(p2);
|
||||
testing.expectEqual(parent, range.endContainer);
|
||||
const beforeOffset = range.endOffset;
|
||||
testing.expectEqual(p2, parent.childNodes[beforeOffset]);
|
||||
|
||||
range.setEndAfter(p2);
|
||||
testing.expectEqual(parent, range.endContainer);
|
||||
const afterOffset = range.endOffset;
|
||||
testing.expectEqual(afterOffset, beforeOffset + 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=cloneRange>
|
||||
{
|
||||
const range = document.createRange();
|
||||
const p1 = $('#p1');
|
||||
const p2 = $('#p2');
|
||||
|
||||
range.setStart(p1, 0);
|
||||
range.setEnd(p2, 1);
|
||||
|
||||
const clone = range.cloneRange();
|
||||
|
||||
testing.expectTrue(clone !== range);
|
||||
testing.expectEqual(range.startContainer, clone.startContainer);
|
||||
testing.expectEqual(range.startOffset, clone.startOffset);
|
||||
testing.expectEqual(range.endContainer, clone.endContainer);
|
||||
testing.expectEqual(range.endOffset, clone.endOffset);
|
||||
testing.expectEqual(range.collapsed, clone.collapsed);
|
||||
|
||||
// Modifying clone shouldn't affect original
|
||||
clone.collapse(true);
|
||||
testing.expectEqual(true, clone.collapsed);
|
||||
testing.expectEqual(false, range.collapsed);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=toString_collapsed>
|
||||
{
|
||||
const range = document.createRange();
|
||||
const p1 = $('#p1');
|
||||
|
||||
range.setStart(p1, 0);
|
||||
range.setEnd(p1, 0);
|
||||
|
||||
testing.expectEqual('', range.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=insertNode>
|
||||
{
|
||||
const range = document.createRange();
|
||||
const p1 = $('#p1');
|
||||
const newSpan = document.createElement('span');
|
||||
newSpan.textContent = 'INSERTED';
|
||||
|
||||
// Select p1's contents
|
||||
range.selectNodeContents(p1);
|
||||
|
||||
// Collapse to start
|
||||
range.collapse(true);
|
||||
|
||||
// Insert node
|
||||
range.insertNode(newSpan);
|
||||
|
||||
// Check that span is first child of p1
|
||||
testing.expectEqual(newSpan, p1.firstChild);
|
||||
testing.expectEqual('INSERTED', newSpan.textContent);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=insertNode_splitText>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.textContent = 'Hello World';
|
||||
|
||||
const range = document.createRange();
|
||||
const textNode = div.firstChild;
|
||||
|
||||
// Set range to middle of text (after "Hello ")
|
||||
range.setStart(textNode, 6);
|
||||
range.collapse(true);
|
||||
|
||||
// Insert a span in the middle
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'MIDDLE';
|
||||
range.insertNode(span);
|
||||
|
||||
// Should have 3 children: "Hello ", span, "World"
|
||||
testing.expectEqual(3, div.childNodes.length);
|
||||
testing.expectEqual('Hello ', div.childNodes[0].textContent);
|
||||
testing.expectEqual(span, div.childNodes[1]);
|
||||
testing.expectEqual('MIDDLE', div.childNodes[1].textContent);
|
||||
testing.expectEqual('World', div.childNodes[2].textContent);
|
||||
|
||||
// Full text should be correct
|
||||
testing.expectEqual('Hello MIDDLEWorld', div.textContent);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=deleteContents>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = '<p>Hello</p><span>World</span>';
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(div);
|
||||
|
||||
testing.expectEqual(2, div.childNodes.length);
|
||||
|
||||
range.deleteContents();
|
||||
|
||||
testing.expectEqual(0, div.childNodes.length);
|
||||
testing.expectEqual(true, range.collapsed);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=cloneContents>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = '<p>First</p><span>Second</span>';
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(div);
|
||||
|
||||
const fragment = range.cloneContents();
|
||||
|
||||
// Original should be unchanged
|
||||
testing.expectEqual(2, div.childNodes.length);
|
||||
|
||||
// Fragment should have copies
|
||||
testing.expectEqual(2, fragment.childNodes.length);
|
||||
testing.expectEqual('P', fragment.childNodes[0].tagName);
|
||||
testing.expectEqual('First', fragment.childNodes[0].textContent);
|
||||
testing.expectEqual('SPAN', fragment.childNodes[1].tagName);
|
||||
testing.expectEqual('Second', fragment.childNodes[1].textContent);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=extractContents>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = '<p>First</p><span>Second</span>';
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(div);
|
||||
|
||||
const fragment = range.extractContents();
|
||||
|
||||
// Original should be empty
|
||||
testing.expectEqual(0, div.childNodes.length);
|
||||
|
||||
// Fragment should have extracted content
|
||||
testing.expectEqual(2, fragment.childNodes.length);
|
||||
testing.expectEqual('P', fragment.childNodes[0].tagName);
|
||||
testing.expectEqual('SPAN', fragment.childNodes[1].tagName);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=surroundContents>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = '<p>Content</p>';
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(div);
|
||||
|
||||
const wrapper = document.createElement('section');
|
||||
wrapper.className = 'wrapper';
|
||||
|
||||
range.surroundContents(wrapper);
|
||||
|
||||
// Div should now contain only the wrapper
|
||||
testing.expectEqual(1, div.childNodes.length);
|
||||
testing.expectEqual(wrapper, div.firstChild);
|
||||
testing.expectEqual('wrapper', wrapper.className);
|
||||
|
||||
// Wrapper should contain original content
|
||||
testing.expectEqual(1, wrapper.childNodes.length);
|
||||
testing.expectEqual('P', wrapper.firstChild.tagName);
|
||||
testing.expectEqual('Content', wrapper.firstChild.textContent);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=createContextualFragment_basic>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(div);
|
||||
|
||||
const fragment = range.createContextualFragment('<p>Hello</p><span>World</span>');
|
||||
|
||||
// Fragment should contain the parsed elements
|
||||
testing.expectEqual(2, fragment.childNodes.length);
|
||||
testing.expectEqual('P', fragment.childNodes[0].tagName);
|
||||
testing.expectEqual('Hello', fragment.childNodes[0].textContent);
|
||||
testing.expectEqual('SPAN', fragment.childNodes[1].tagName);
|
||||
testing.expectEqual('World', fragment.childNodes[1].textContent);
|
||||
|
||||
// Original div should be unchanged
|
||||
testing.expectEqual(0, div.childNodes.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=createContextualFragment_empty>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(div);
|
||||
|
||||
const fragment = range.createContextualFragment('');
|
||||
|
||||
testing.expectEqual(0, fragment.childNodes.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=createContextualFragment_textContext>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.textContent = 'Some text';
|
||||
|
||||
const range = document.createRange();
|
||||
const textNode = div.firstChild;
|
||||
range.setStart(textNode, 5);
|
||||
range.collapse(true);
|
||||
|
||||
// Even though range is in text node, should use parent (div) as context
|
||||
const fragment = range.createContextualFragment('<b>Bold</b>');
|
||||
|
||||
testing.expectEqual(1, fragment.childNodes.length);
|
||||
testing.expectEqual('B', fragment.childNodes[0].tagName);
|
||||
testing.expectEqual('Bold', fragment.childNodes[0].textContent);
|
||||
}
|
||||
</script>
|
||||
@@ -177,6 +177,11 @@ pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node
|
||||
return page.createTextNode(data);
|
||||
}
|
||||
|
||||
const Range = @import("Range.zig");
|
||||
pub fn createRange(_: *const Document, page: *Page) !*Range {
|
||||
return Range.init(page);
|
||||
}
|
||||
|
||||
pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@import("Event.zig") {
|
||||
const Event = @import("Event.zig");
|
||||
|
||||
@@ -290,6 +295,7 @@ pub const JsApi = struct {
|
||||
pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{});
|
||||
pub const createComment = bridge.function(Document.createComment, .{});
|
||||
pub const createTextNode = bridge.function(Document.createTextNode, .{});
|
||||
pub const createRange = bridge.function(Document.createRange, .{});
|
||||
pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true });
|
||||
pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{});
|
||||
pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{});
|
||||
|
||||
@@ -419,6 +419,61 @@ pub fn childrenIterator(self: *Node) NodeIterator {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getLength(self: *Node) u32 {
|
||||
switch (self._type) {
|
||||
.cdata => |cdata| {
|
||||
return @intCast(cdata.getData().len);
|
||||
},
|
||||
.element, .document, .document_fragment => {
|
||||
var count: u32 = 0;
|
||||
var it = self.childrenIterator();
|
||||
while (it.next()) |_| {
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
},
|
||||
.document_type, .attribute => return 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getChildIndex(self: *Node, target: *const Node) ?u32 {
|
||||
var i: u32 = 0;
|
||||
var it = self.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
if (child == target) {
|
||||
return i;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getChildAt(self: *Node, index: u32) ?*Node {
|
||||
var i: u32 = 0;
|
||||
var it = self.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
if (i == index) {
|
||||
return child;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getData(self: *const Node) []const u8 {
|
||||
return switch (self._type) {
|
||||
.cdata => |c| c.getData(),
|
||||
else => "",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setData(self: *Node, data: []const u8) void {
|
||||
switch (self._type) {
|
||||
.cdata => |c| c._data = data,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn className(self: *const Node) []const u8 {
|
||||
switch (self._type) {
|
||||
inline else => |c| return c.className(),
|
||||
|
||||
493
src/browser/webapi/Range.zig
Normal file
493
src/browser/webapi/Range.zig
Normal file
@@ -0,0 +1,493 @@
|
||||
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const Node = @import("Node.zig");
|
||||
const DocumentFragment = @import("DocumentFragment.zig");
|
||||
|
||||
const Range = @This();
|
||||
|
||||
_end_offset: u32,
|
||||
_start_offset: u32,
|
||||
_end_container: *Node,
|
||||
_start_container: *Node,
|
||||
|
||||
pub fn init(page: *Page) !*Range {
|
||||
// Per spec, a new range starts collapsed at the document's first position
|
||||
const doc = page.document.asNode();
|
||||
return page._factory.create(Range{
|
||||
._end_offset = 0,
|
||||
._start_offset = 0,
|
||||
._end_container = doc,
|
||||
._start_container = doc,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn getStartContainer(self: *const Range) *Node {
|
||||
return self._start_container;
|
||||
}
|
||||
|
||||
pub fn getStartOffset(self: *const Range) u32 {
|
||||
return self._start_offset;
|
||||
}
|
||||
|
||||
pub fn getEndContainer(self: *const Range) *Node {
|
||||
return self._end_container;
|
||||
}
|
||||
|
||||
pub fn getEndOffset(self: *const Range) u32 {
|
||||
return self._end_offset;
|
||||
}
|
||||
|
||||
pub fn getCollapsed(self: *const Range) bool {
|
||||
return self._start_container == self._end_container and
|
||||
self._start_offset == self._end_offset;
|
||||
}
|
||||
|
||||
pub fn setStart(self: *Range, node: *Node, offset: u32) !void {
|
||||
self._start_container = node;
|
||||
self._start_offset = offset;
|
||||
|
||||
// If start is now after end, collapse to start
|
||||
if (self.isStartAfterEnd()) {
|
||||
self._end_container = self._start_container;
|
||||
self._end_offset = self._start_offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setEnd(self: *Range, node: *Node, offset: u32) !void {
|
||||
self._end_container = node;
|
||||
self._end_offset = offset;
|
||||
|
||||
// If end is now before start, collapse to end
|
||||
if (self.isStartAfterEnd()) {
|
||||
self._start_container = self._end_container;
|
||||
self._start_offset = self._end_offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setStartBefore(self: *Range, node: *Node) !void {
|
||||
const parent = node.parentNode() orelse return error.InvalidNodeType;
|
||||
const offset = parent.getChildIndex(node) orelse return error.NotFound;
|
||||
try self.setStart(parent, offset);
|
||||
}
|
||||
|
||||
pub fn setStartAfter(self: *Range, node: *Node) !void {
|
||||
const parent = node.parentNode() orelse return error.InvalidNodeType;
|
||||
const offset = parent.getChildIndex(node) orelse return error.NotFound;
|
||||
try self.setStart(parent, offset + 1);
|
||||
}
|
||||
|
||||
pub fn setEndBefore(self: *Range, node: *Node) !void {
|
||||
const parent = node.parentNode() orelse return error.InvalidNodeType;
|
||||
const offset = parent.getChildIndex(node) orelse return error.NotFound;
|
||||
try self.setEnd(parent, offset);
|
||||
}
|
||||
|
||||
pub fn setEndAfter(self: *Range, node: *Node) !void {
|
||||
const parent = node.parentNode() orelse return error.InvalidNodeType;
|
||||
const offset = parent.getChildIndex(node) orelse return error.NotFound;
|
||||
try self.setEnd(parent, offset + 1);
|
||||
}
|
||||
|
||||
pub fn selectNode(self: *Range, node: *Node) !void {
|
||||
const parent = node.parentNode() orelse return error.InvalidNodeType;
|
||||
const offset = parent.getChildIndex(node) orelse return error.NotFound;
|
||||
try self.setStart(parent, offset);
|
||||
try self.setEnd(parent, offset + 1);
|
||||
}
|
||||
|
||||
pub fn selectNodeContents(self: *Range, node: *Node) !void {
|
||||
const length = node.getLength();
|
||||
try self.setStart(node, 0);
|
||||
try self.setEnd(node, length);
|
||||
}
|
||||
|
||||
pub fn collapse(self: *Range, to_start: ?bool) void {
|
||||
if (to_start orelse true) {
|
||||
self._end_container = self._start_container;
|
||||
self._end_offset = self._start_offset;
|
||||
} else {
|
||||
self._start_container = self._end_container;
|
||||
self._start_offset = self._end_offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cloneRange(self: *const Range, page: *Page) !*Range {
|
||||
return page._factory.create(Range{
|
||||
._end_offset = self._end_offset,
|
||||
._start_offset = self._start_offset,
|
||||
._end_container = self._end_container,
|
||||
._start_container = self._start_container,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insertNode(self: *Range, node: *Node, page: *Page) !void {
|
||||
// Insert node at the start of the range
|
||||
const container = self._start_container;
|
||||
const offset = self._start_offset;
|
||||
|
||||
if (container.is(Node.CData)) |_| {
|
||||
// If container is a text node, we need to split it
|
||||
const parent = container.parentNode() orelse return error.InvalidNodeType;
|
||||
|
||||
if (offset == 0) {
|
||||
_ = try parent.insertBefore(node, container, page);
|
||||
} else {
|
||||
const text_data = container.getData();
|
||||
if (offset >= text_data.len) {
|
||||
_ = try parent.insertBefore(node, container.nextSibling(), page);
|
||||
} else {
|
||||
// Split the text node into before and after parts
|
||||
const before_text = text_data[0..offset];
|
||||
const after_text = text_data[offset..];
|
||||
|
||||
const before = try page.createTextNode(before_text);
|
||||
const after = try page.createTextNode(after_text);
|
||||
|
||||
_ = try parent.replaceChild(before, container, page);
|
||||
_ = try parent.insertBefore(node, before.nextSibling(), page);
|
||||
_ = try parent.insertBefore(after, node.nextSibling(), page);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Container is an element, insert at offset
|
||||
const ref_child = container.getChildAt(offset);
|
||||
_ = try container.insertBefore(node, ref_child, page);
|
||||
}
|
||||
|
||||
// Update range to be after the inserted node
|
||||
if (self._start_container == self._end_container) {
|
||||
self._end_offset += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deleteContents(self: *Range, page: *Page) !void {
|
||||
if (self.getCollapsed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple case: same container
|
||||
if (self._start_container == self._end_container) {
|
||||
if (self._start_container.is(Node.CData)) |_| {
|
||||
// Delete part of text node
|
||||
const text_data = self._start_container.getData();
|
||||
const new_text = try std.mem.concat(
|
||||
page.arena,
|
||||
u8,
|
||||
&.{ text_data[0..self._start_offset], text_data[self._end_offset..] },
|
||||
);
|
||||
self._start_container.setData(new_text);
|
||||
} else {
|
||||
// Delete child nodes in range
|
||||
var offset = self._start_offset;
|
||||
while (offset < self._end_offset) : (offset += 1) {
|
||||
if (self._start_container.getChildAt(self._start_offset)) |child| {
|
||||
_ = try self._start_container.removeChild(child, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.collapse(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Complex case: different containers - simplified implementation
|
||||
// Just collapse the range for now
|
||||
self.collapse(true);
|
||||
}
|
||||
|
||||
pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
|
||||
const fragment = try DocumentFragment.init(page);
|
||||
|
||||
if (self.getCollapsed()) return fragment;
|
||||
|
||||
// Simple case: same container
|
||||
if (self._start_container == self._end_container) {
|
||||
if (self._start_container.is(Node.CData)) |_| {
|
||||
// Clone part of text node
|
||||
const text_data = self._start_container.getData();
|
||||
if (self._start_offset < text_data.len and self._end_offset <= text_data.len) {
|
||||
const cloned_text = text_data[self._start_offset..self._end_offset];
|
||||
const text_node = try page.createTextNode(cloned_text);
|
||||
_ = try fragment.asNode().appendChild(text_node, page);
|
||||
}
|
||||
} else {
|
||||
// Clone child nodes in range
|
||||
var offset = self._start_offset;
|
||||
while (offset < self._end_offset) : (offset += 1) {
|
||||
if (self._start_container.getChildAt(offset)) |child| {
|
||||
const cloned = try child.cloneNode(true, page);
|
||||
_ = try fragment.asNode().appendChild(cloned, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
pub fn extractContents(self: *Range, page: *Page) !*DocumentFragment {
|
||||
const fragment = try self.cloneContents(page);
|
||||
try self.deleteContents(page);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
pub fn surroundContents(self: *Range, new_parent: *Node, page: *Page) !void {
|
||||
// Extract contents
|
||||
const contents = try self.extractContents(page);
|
||||
|
||||
// Insert the new parent
|
||||
try self.insertNode(new_parent, page);
|
||||
|
||||
// Move contents into new parent
|
||||
_ = try new_parent.appendChild(contents.asNode(), page);
|
||||
|
||||
// Select the new parent's contents
|
||||
try self.selectNodeContents(new_parent);
|
||||
}
|
||||
|
||||
pub fn createContextualFragment(self: *const Range, html: []const u8, page: *Page) !*DocumentFragment {
|
||||
var context_node = self._start_container;
|
||||
|
||||
// If start container is a text node, use its parent as context
|
||||
if (context_node.is(Node.CData)) |_| {
|
||||
context_node = context_node.parentNode() orelse context_node;
|
||||
}
|
||||
|
||||
const fragment = try DocumentFragment.init(page);
|
||||
|
||||
if (html.len == 0) {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
// Create a temporary element of the same type as the context for parsing
|
||||
// This preserves the parsing context without modifying the original node
|
||||
const temp_node = if (context_node.is(Node.Element)) |el|
|
||||
try page.createElement(el._namespace.toUri(), el.getTagNameLower(), null)
|
||||
else
|
||||
try page.createElement(null, "div", null);
|
||||
|
||||
try page.parseHtmlAsChildren(temp_node, html);
|
||||
|
||||
// Move all parsed children to the fragment
|
||||
// Keep removing first child until temp element is empty
|
||||
const fragment_node = fragment.asNode();
|
||||
while (temp_node.firstChild()) |child| {
|
||||
page.removeNode(temp_node, child, .{ .will_be_reconnected = true });
|
||||
try page.appendNode(fragment_node, child, .{ .child_already_connected = false });
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
pub fn toString(self: *const Range, page: *Page) ![]const u8 {
|
||||
// Simplified implementation: just extract text content
|
||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try self.writeTextContent(&buf.writer);
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
||||
if (self.getCollapsed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self._start_container == self._end_container) {
|
||||
if (self._start_container.is(Node.CData)) |cdata| {
|
||||
const data = cdata.getData();
|
||||
if (self._start_offset < data.len and self._end_offset <= data.len) {
|
||||
try writer.writeAll(data[self._start_offset..self._end_offset]);
|
||||
}
|
||||
}
|
||||
// For elements, would need to iterate children
|
||||
return;
|
||||
}
|
||||
|
||||
// Complex case: different containers - would need proper tree walking
|
||||
// For now, just return empty
|
||||
}
|
||||
|
||||
fn isStartAfterEnd(self: *const Range) bool {
|
||||
return compareBoundaryPoints(
|
||||
self._start_container,
|
||||
self._start_offset,
|
||||
self._end_container,
|
||||
self._end_offset,
|
||||
) == .after;
|
||||
}
|
||||
|
||||
const BoundaryComparison = enum {
|
||||
before,
|
||||
equal,
|
||||
after,
|
||||
};
|
||||
|
||||
/// Compare two boundary points in tree order
|
||||
/// Returns whether (nodeA, offsetA) is before/equal/after (nodeB, offsetB)
|
||||
fn compareBoundaryPoints(
|
||||
node_a: *Node,
|
||||
offset_a: u32,
|
||||
node_b: *Node,
|
||||
offset_b: u32,
|
||||
) BoundaryComparison {
|
||||
// If same container, just compare offsets
|
||||
if (node_a == node_b) {
|
||||
if (offset_a < offset_b) return .before;
|
||||
if (offset_a > offset_b) return .after;
|
||||
return .equal;
|
||||
}
|
||||
|
||||
// Check if one contains the other
|
||||
if (isAncestorOf(node_a, node_b)) {
|
||||
// A contains B, so A's position comes before B
|
||||
// But we need to check if the offset in A comes after B
|
||||
var child = node_b;
|
||||
var parent = child.parentNode();
|
||||
while (parent) |p| {
|
||||
if (p == node_a) {
|
||||
const child_index = p.getChildIndex(child) orelse unreachable;
|
||||
if (offset_a <= child_index) {
|
||||
return .before;
|
||||
}
|
||||
return .after;
|
||||
}
|
||||
child = p;
|
||||
parent = p.parentNode();
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
|
||||
if (isAncestorOf(node_b, node_a)) {
|
||||
// B contains A, so B's position comes before A
|
||||
var child = node_a;
|
||||
var parent = child.parentNode();
|
||||
while (parent) |p| {
|
||||
if (p == node_b) {
|
||||
const child_index = p.getChildIndex(child) orelse unreachable;
|
||||
if (child_index < offset_b) {
|
||||
return .before;
|
||||
}
|
||||
return .after;
|
||||
}
|
||||
child = p;
|
||||
parent = p.parentNode();
|
||||
}
|
||||
unreachable;
|
||||
}
|
||||
|
||||
// Neither contains the other, find their relative position in tree order
|
||||
// Walk up from A to find all ancestors
|
||||
var current = node_a;
|
||||
var a_count: usize = 0;
|
||||
var a_ancestors: [64]*Node = undefined;
|
||||
while (a_count < 64) {
|
||||
a_ancestors[a_count] = current;
|
||||
a_count += 1;
|
||||
current = current.parentNode() orelse break;
|
||||
}
|
||||
|
||||
// Walk up from B and find first common ancestor
|
||||
current = node_b;
|
||||
while (current.parentNode()) |parent| {
|
||||
for (a_ancestors[0..a_count]) |ancestor| {
|
||||
if (ancestor != parent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found common ancestor
|
||||
// Now compare positions of the children in this ancestor
|
||||
const a_child = blk: {
|
||||
var node = node_a;
|
||||
while (node.parentNode()) |p| {
|
||||
if (p == parent) break :blk node;
|
||||
node = p;
|
||||
}
|
||||
unreachable;
|
||||
};
|
||||
const b_child = current;
|
||||
|
||||
const a_index = parent.getChildIndex(a_child) orelse unreachable;
|
||||
const b_index = parent.getChildIndex(b_child) orelse unreachable;
|
||||
|
||||
if (a_index < b_index) {
|
||||
return .before;
|
||||
}
|
||||
if (a_index > b_index) {
|
||||
return .after;
|
||||
}
|
||||
return .equal;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
|
||||
// Should not reach here if nodes are in the same tree
|
||||
return .before;
|
||||
}
|
||||
|
||||
fn isAncestorOf(potential_ancestor: *Node, node: *Node) bool {
|
||||
var current = node.parentNode();
|
||||
while (current) |parent| {
|
||||
if (parent == potential_ancestor) {
|
||||
return true;
|
||||
}
|
||||
current = parent.parentNode();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Range);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "Range";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(Range.init, .{});
|
||||
pub const startContainer = bridge.accessor(Range.getStartContainer, null, .{});
|
||||
pub const startOffset = bridge.accessor(Range.getStartOffset, null, .{});
|
||||
pub const endContainer = bridge.accessor(Range.getEndContainer, null, .{});
|
||||
pub const endOffset = bridge.accessor(Range.getEndOffset, null, .{});
|
||||
pub const collapsed = bridge.accessor(Range.getCollapsed, null, .{});
|
||||
pub const setStart = bridge.function(Range.setStart, .{});
|
||||
pub const setEnd = bridge.function(Range.setEnd, .{});
|
||||
pub const setStartBefore = bridge.function(Range.setStartBefore, .{});
|
||||
pub const setStartAfter = bridge.function(Range.setStartAfter, .{});
|
||||
pub const setEndBefore = bridge.function(Range.setEndBefore, .{});
|
||||
pub const setEndAfter = bridge.function(Range.setEndAfter, .{});
|
||||
pub const selectNode = bridge.function(Range.selectNode, .{});
|
||||
pub const selectNodeContents = bridge.function(Range.selectNodeContents, .{});
|
||||
pub const collapse = bridge.function(Range.collapse, .{});
|
||||
pub const cloneRange = bridge.function(Range.cloneRange, .{});
|
||||
pub const insertNode = bridge.function(Range.insertNode, .{});
|
||||
pub const deleteContents = bridge.function(Range.deleteContents, .{});
|
||||
pub const cloneContents = bridge.function(Range.cloneContents, .{});
|
||||
pub const extractContents = bridge.function(Range.extractContents, .{});
|
||||
pub const surroundContents = bridge.function(Range.surroundContents, .{});
|
||||
pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{});
|
||||
pub const toString = bridge.function(Range.toString, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "WebApi: Range" {
|
||||
try testing.htmlRunner("range.html", .{});
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../..//log.zig");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const Node = @import("../Node.zig");
|
||||
@@ -63,6 +64,23 @@ pub fn entries(self: *NodeList, page: *Page) !*EntryIterator {
|
||||
return .init(.{ .list = self }, page);
|
||||
}
|
||||
|
||||
pub fn forEach(self: *NodeList, cb: js.Function, page: *Page) !void {
|
||||
var i: i32 = 0;
|
||||
var it = try self.values(page);
|
||||
while (true) : (i += 1) {
|
||||
const next = try it.next(page);
|
||||
if (next.done) {
|
||||
return;
|
||||
}
|
||||
|
||||
var result: js.Function.Result = undefined;
|
||||
cb.tryCall(void, .{ next.value, i, self }, &result) catch {
|
||||
log.debug(.js, "forEach callback", .{ .err = result.exception, .stack = result.stack });
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const GenericIterator = @import("iterator.zig").Entry;
|
||||
pub const KeyIterator = GenericIterator(Iterator, "0");
|
||||
pub const ValueIterator = GenericIterator(Iterator, "1");
|
||||
@@ -96,5 +114,6 @@ pub const JsApi = struct {
|
||||
pub const keys = bridge.function(NodeList.keys, .{});
|
||||
pub const values = bridge.function(NodeList.values, .{});
|
||||
pub const entries = bridge.function(NodeList.entries, .{});
|
||||
pub const forEach = bridge.function(NodeList.forEach, .{});
|
||||
pub const symbol_iterator = bridge.iterator(NodeList.values, .{});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user