Properly handle insertion of DocumentFragment

Add various CData methods

XHR and Fetch request headers

Animation mocks
This commit is contained in:
Karl Seguin
2025-12-04 14:39:15 +08:00
parent 7cb06f3e58
commit c9882e10a4
19 changed files with 1288 additions and 48 deletions

View File

@@ -1431,6 +1431,24 @@ pub fn appendAllChildren(self: *Page, parent: *Node, target: *Node) !void {
}
}
pub fn insertAllChildrenBefore(self: *Page, fragment: *Node, target: *Node, ref_node: *Node) !void {
self.domChanged();
const dest_connected = target.isConnected();
var it = fragment.childrenIterator();
while (it.next()) |child| {
// Check if child was connected BEFORE removing it from fragment
const child_was_connected = child.isConnected();
self.removeNode(fragment, child, .{ .will_be_reconnected = dest_connected });
try self.insertNodeRelative(
target,
child,
.{ .before = ref_node },
.{ .child_already_connected = child_was_connected },
);
}
}
fn _appendNode(self: *Page, comptime from_parser: bool, parent: *Node, child: *Node, opts: InsertNodeOpts) !void {
self._insertNodeRelative(from_parser, parent, child, .append, opts);
}

View File

@@ -135,7 +135,7 @@ pub fn isNullOrUndefined(self: Object) bool {
return self.js_obj.toValue().isNullOrUndefined();
}
pub fn nameIterator(self: Object, allocator: Allocator) NameIterator {
pub fn nameIterator(self: Object) NameIterator {
const context = self.context;
const js_obj = self.js_obj;
@@ -145,7 +145,6 @@ pub fn nameIterator(self: Object, allocator: Allocator) NameIterator {
return .{
.count = count,
.context = context,
.allocator = allocator,
.js_obj = array.castTo(v8.Object),
};
}
@@ -158,7 +157,6 @@ pub const NameIterator = struct {
count: u32,
idx: u32 = 0,
js_obj: v8.Object,
allocator: Allocator,
context: *const Context,
pub fn next(self: *NameIterator) !?[]const u8 {
@@ -170,6 +168,6 @@ pub const NameIterator = struct {
const context = self.context;
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx);
return try context.valueToString(js_val, .{ .allocator = self.allocator });
return try context.valueToString(js_val, .{});
}
};

View File

@@ -568,6 +568,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/media/MediaError.zig"),
@import("../webapi/media/TextTrackCue.zig"),
@import("../webapi/media/VTTCue.zig"),
@import("../webapi/animation/Animation.zig"),
@import("../webapi/EventTarget.zig"),
@import("../webapi/Location.zig"),
@import("../webapi/Navigator.zig"),

View File

@@ -0,0 +1,730 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="container"></div>
<script id="lengthProperty">
{
// length property
const text = document.createTextNode('Hello');
testing.expectEqual(5, text.length);
testing.expectEqual(5, text.data.length);
const empty = document.createTextNode('');
testing.expectEqual(0, empty.length);
const comment = document.createComment('test comment');
testing.expectEqual(12, comment.length);
}
</script>
<script id="appendDataBasic">
{
// appendData basic
const text = document.createTextNode('Hello');
text.appendData(' World');
testing.expectEqual('Hello World', text.data);
testing.expectEqual(11, text.length);
}
</script>
<script id="appendDataEmpty">
{
// appendData to empty
const text = document.createTextNode('');
text.appendData('First');
testing.expectEqual('First', text.data);
// appendData empty string
text.appendData('');
testing.expectEqual('First', text.data);
}
</script>
<script id="deleteDataBasic">
{
// deleteData from middle
const text = document.createTextNode('Hello World');
text.deleteData(5, 6); // Remove ' World'
testing.expectEqual('Hello', text.data);
testing.expectEqual(5, text.length);
}
</script>
<script id="deleteDataStart">
{
// deleteData from start
const text = document.createTextNode('Hello World');
text.deleteData(0, 6); // Remove 'Hello '
testing.expectEqual('World', text.data);
}
</script>
<script id="deleteDataEnd">
{
// deleteData from end
const text = document.createTextNode('Hello World');
text.deleteData(5, 100); // Remove ' World' (count exceeds length)
testing.expectEqual('Hello', text.data);
}
</script>
<script id="deleteDataAll">
{
// deleteData everything
const text = document.createTextNode('Hello');
text.deleteData(0, 5);
testing.expectEqual('', text.data);
testing.expectEqual(0, text.length);
}
</script>
<script id="deleteDataZeroCount">
{
// deleteData with count=0
const text = document.createTextNode('Hello');
text.deleteData(2, 0);
testing.expectEqual('Hello', text.data);
}
</script>
<script id="deleteDataInvalidOffset">
{
// deleteData with invalid offset
const text = document.createTextNode('Hello');
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => text.deleteData(10, 5));
testing.expectEqual('Hello', text.data); // unchanged
}
</script>
<script id="insertDataMiddle">
{
// insertData in middle
const text = document.createTextNode('Hello');
text.insertData(5, ' World');
testing.expectEqual('Hello World', text.data);
}
</script>
<script id="insertDataStart">
{
// insertData at start
const text = document.createTextNode('World');
text.insertData(0, 'Hello ');
testing.expectEqual('Hello World', text.data);
}
</script>
<script id="insertDataEnd">
{
// insertData at end
const text = document.createTextNode('Hello');
text.insertData(5, ' World');
testing.expectEqual('Hello World', text.data);
}
</script>
<script id="insertDataEmpty">
{
// insertData into empty
const text = document.createTextNode('');
text.insertData(0, 'Hello');
testing.expectEqual('Hello', text.data);
}
</script>
<script id="insertDataInvalidOffset">
{
// insertData with invalid offset
const text = document.createTextNode('Hello');
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => text.insertData(10, 'X'));
testing.expectEqual('Hello', text.data);
}
</script>
<script id="replaceDataBasic">
{
// replaceData basic
const text = document.createTextNode('Hello World');
text.replaceData(6, 5, 'Universe');
testing.expectEqual('Hello Universe', text.data);
}
</script>
<script id="replaceDataShorter">
{
// replaceData with shorter string
const text = document.createTextNode('Hello World');
text.replaceData(6, 5, 'Hi');
testing.expectEqual('Hello Hi', text.data);
}
</script>
<script id="replaceDataLonger">
{
// replaceData with longer string
const text = document.createTextNode('Hello Hi');
text.replaceData(6, 2, 'World');
testing.expectEqual('Hello World', text.data);
}
</script>
<script id="replaceDataExceedingCount">
{
// replaceData with count exceeding length
const text = document.createTextNode('Hello World');
text.replaceData(6, 100, 'Everyone');
testing.expectEqual('Hello Everyone', text.data);
}
</script>
<script id="replaceDataZeroCount">
{
// replaceData with count=0 (acts like insert)
const text = document.createTextNode('Hello World');
text.replaceData(5, 0, '!!!');
testing.expectEqual('Hello!!! World', text.data);
}
</script>
<script id="substringDataBasic">
{
// substringData basic
const text = document.createTextNode('Hello World');
const sub = text.substringData(0, 5);
testing.expectEqual('Hello', sub);
testing.expectEqual('Hello World', text.data); // original unchanged
}
</script>
<script id="substringDataMiddle">
{
// substringData from middle
const text = document.createTextNode('Hello World');
const sub = text.substringData(6, 5);
testing.expectEqual('World', sub);
}
</script>
<script id="substringDataExceedingCount">
{
// substringData with count exceeding length
const text = document.createTextNode('Hello World');
const sub = text.substringData(6, 100);
testing.expectEqual('World', sub);
}
</script>
<script id="substringDataZeroCount">
{
// substringData with count=0
const text = document.createTextNode('Hello');
const sub = text.substringData(0, 0);
testing.expectEqual('', sub);
}
</script>
<script id="substringDataInvalidOffset">
{
// substringData with invalid offset
const text = document.createTextNode('Hello');
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => text.substringData(10, 5));
}
</script>
<script id="commentCharacterData">
{
// CharacterData methods work on comments too
const comment = document.createComment('Hello');
comment.appendData(' World');
testing.expectEqual('Hello World', comment.data);
comment.deleteData(5, 6);
testing.expectEqual('Hello', comment.data);
comment.insertData(0, 'Start: ');
testing.expectEqual('Start: Hello', comment.data);
comment.replaceData(0, 7, 'End: ');
testing.expectEqual('End: Hello', comment.data);
const sub = comment.substringData(5, 5);
testing.expectEqual('Hello', sub);
}
</script>
<script id="dataChangeNotifications">
{
// Verify data changes are reflected in DOM
const container = $('#container');
container.innerHTML = '';
const text = document.createTextNode('Original');
container.appendChild(text);
text.appendData(' Text');
testing.expectEqual('Original Text', container.textContent);
text.deleteData(0, 9);
testing.expectEqual('Text', container.textContent);
text.data = 'Changed';
testing.expectEqual('Changed', container.textContent);
}
</script>
<script id="removeWithSiblings">
{
// remove() when node has siblings
const container = $('#container');
container.innerHTML = '';
const text1 = document.createTextNode('A');
const text2 = document.createTextNode('B');
const text3 = document.createTextNode('C');
container.appendChild(text1);
container.appendChild(text2);
container.appendChild(text3);
testing.expectEqual(3, container.childNodes.length);
testing.expectEqual('ABC', container.textContent);
text2.remove();
testing.expectEqual(2, container.childNodes.length);
testing.expectEqual('AC', container.textContent);
testing.expectEqual(null, text2.parentNode);
testing.expectEqual(text3, text1.nextSibling);
}
</script>
<script id="removeOnlyChild">
{
// remove() when node is only child
const container = $('#container');
container.innerHTML = '';
const text = document.createTextNode('Only');
container.appendChild(text);
testing.expectEqual(1, container.childNodes.length);
text.remove();
testing.expectEqual(0, container.childNodes.length);
testing.expectEqual(null, text.parentNode);
}
</script>
<script id="removeNoParent">
{
// remove() when node has no parent (should do nothing)
const text = document.createTextNode('Orphan');
text.remove(); // Should not throw
testing.expectEqual(null, text.parentNode);
}
</script>
<script id="removeCommentWithElementSiblings">
{
// remove() comment with element siblings
const container = $('#container');
container.innerHTML = '';
const div1 = document.createElement('div');
div1.textContent = 'First';
const comment = document.createComment('middle');
const div2 = document.createElement('div');
div2.textContent = 'Last';
container.appendChild(div1);
container.appendChild(comment);
container.appendChild(div2);
testing.expectEqual(3, container.childNodes.length);
comment.remove();
testing.expectEqual(2, container.childNodes.length);
testing.expectEqual('DIV', container.childNodes[0].tagName);
testing.expectEqual('DIV', container.childNodes[1].tagName);
testing.expectEqual(div2, div1.nextSibling);
}
</script>
<script id="beforeWithSiblings">
{
// before() when node has siblings
const container = $('#container');
container.innerHTML = '';
const text1 = document.createTextNode('A');
const text2 = document.createTextNode('C');
container.appendChild(text1);
container.appendChild(text2);
const textB = document.createTextNode('B');
text2.before(textB);
testing.expectEqual(3, container.childNodes.length);
testing.expectEqual('ABC', container.textContent);
testing.expectEqual(textB, text1.nextSibling);
testing.expectEqual(text2, textB.nextSibling);
}
</script>
<script id="beforeMultipleNodes">
{
// before() with multiple nodes
const container = $('#container');
container.innerHTML = '';
const text = document.createTextNode('Z');
container.appendChild(text);
const text1 = document.createTextNode('A');
const text2 = document.createTextNode('B');
const text3 = document.createTextNode('C');
text.before(text1, text2, text3);
testing.expectEqual(4, container.childNodes.length);
testing.expectEqual('ABCZ', container.textContent);
}
</script>
<script id="beforeMixedTypes">
{
// before() with mixed node types
const container = $('#container');
container.innerHTML = '';
const target = document.createTextNode('Target');
container.appendChild(target);
const elem = document.createElement('span');
elem.textContent = 'E';
const text = document.createTextNode('T');
const comment = document.createComment('C');
target.before(elem, text, comment);
testing.expectEqual(4, container.childNodes.length);
testing.expectEqual(1, container.childNodes[0].nodeType); // ELEMENT_NODE
testing.expectEqual(3, container.childNodes[1].nodeType); // TEXT_NODE
testing.expectEqual(8, container.childNodes[2].nodeType); // COMMENT_NODE
testing.expectEqual(3, container.childNodes[3].nodeType); // TEXT_NODE (target)
}
</script>
<script id="beforeNoParent">
{
// before() when node has no parent (should do nothing)
const orphan = document.createTextNode('Orphan');
const text = document.createTextNode('Test');
orphan.before(text);
testing.expectEqual(null, orphan.parentNode);
testing.expectEqual(null, text.parentNode);
}
</script>
<script id="beforeOnlyChild">
{
// before() when target is only child
const container = $('#container');
container.innerHTML = '';
const target = document.createTextNode('B');
container.appendChild(target);
const textA = document.createTextNode('A');
target.before(textA);
testing.expectEqual(2, container.childNodes.length);
testing.expectEqual('AB', container.textContent);
testing.expectEqual(textA, container.firstChild);
testing.expectEqual(target, textA.nextSibling);
}
</script>
<script id="afterWithSiblings">
{
// after() when node has siblings
const container = $('#container');
container.innerHTML = '';
const text1 = document.createTextNode('A');
const text2 = document.createTextNode('C');
container.appendChild(text1);
container.appendChild(text2);
const textB = document.createTextNode('B');
text1.after(textB);
testing.expectEqual(3, container.childNodes.length);
testing.expectEqual('ABC', container.textContent);
testing.expectEqual(textB, text1.nextSibling);
testing.expectEqual(text2, textB.nextSibling);
}
</script>
<script id="afterMultipleNodes">
{
// after() with multiple nodes
const container = $('#container');
container.innerHTML = '';
const text = document.createTextNode('A');
container.appendChild(text);
const text1 = document.createTextNode('B');
const text2 = document.createTextNode('C');
const text3 = document.createTextNode('D');
text.after(text1, text2, text3);
testing.expectEqual(4, container.childNodes.length);
testing.expectEqual('ABCD', container.textContent);
}
</script>
<script id="afterMixedTypes">
{
// after() with mixed node types
const container = $('#container');
container.innerHTML = '';
const target = document.createTextNode('Start');
container.appendChild(target);
const elem = document.createElement('div');
elem.textContent = 'E';
const comment = document.createComment('comment');
const text = document.createTextNode('T');
target.after(elem, comment, text);
testing.expectEqual(4, container.childNodes.length);
testing.expectEqual(3, container.childNodes[0].nodeType); // TEXT_NODE (target)
testing.expectEqual(1, container.childNodes[1].nodeType); // ELEMENT_NODE
testing.expectEqual(8, container.childNodes[2].nodeType); // COMMENT_NODE
testing.expectEqual(3, container.childNodes[3].nodeType); // TEXT_NODE
}
</script>
<script id="afterNoParent">
{
// after() when node has no parent (should do nothing)
const orphan = document.createTextNode('Orphan');
const text = document.createTextNode('Test');
orphan.after(text);
testing.expectEqual(null, orphan.parentNode);
testing.expectEqual(null, text.parentNode);
}
</script>
<script id="afterAsLastChild">
{
// after() when target is last child
const container = $('#container');
container.innerHTML = '';
const target = document.createTextNode('A');
container.appendChild(target);
const textB = document.createTextNode('B');
target.after(textB);
testing.expectEqual(2, container.childNodes.length);
testing.expectEqual('AB', container.textContent);
testing.expectEqual(target, container.firstChild);
testing.expectEqual(textB, container.lastChild);
testing.expectEqual(null, textB.nextSibling);
}
</script>
<script id="replaceWithSingleNode">
{
// replaceWith() with single node
const container = $('#container');
container.innerHTML = '';
const old = document.createTextNode('Old');
container.appendChild(old);
const replacement = document.createTextNode('New');
old.replaceWith(replacement);
testing.expectEqual(1, container.childNodes.length);
testing.expectEqual('New', container.textContent);
testing.expectEqual(null, old.parentNode);
testing.expectEqual(container, replacement.parentNode);
}
</script>
<script id="replaceWithMultipleNodes">
{
// replaceWith() with multiple nodes
const container = $('#container');
container.innerHTML = '';
const old = document.createTextNode('X');
container.appendChild(old);
const text1 = document.createTextNode('A');
const text2 = document.createTextNode('B');
const text3 = document.createTextNode('C');
old.replaceWith(text1, text2, text3);
testing.expectEqual(3, container.childNodes.length);
testing.expectEqual('ABC', container.textContent);
testing.expectEqual(null, old.parentNode);
}
</script>
<script id="replaceWithOnlyChild">
{
// replaceWith() when target is only child
const container = $('#container');
container.innerHTML = '';
const old = document.createTextNode('Only');
container.appendChild(old);
testing.expectEqual(1, container.childNodes.length);
const replacement = document.createTextNode('Replaced');
old.replaceWith(replacement);
testing.expectEqual(1, container.childNodes.length);
testing.expectEqual('Replaced', container.textContent);
testing.expectEqual(replacement, container.firstChild);
}
</script>
<script id="replaceWithBetweenSiblings">
{
// replaceWith() when node has siblings on both sides
const container = $('#container');
container.innerHTML = '';
const text1 = document.createTextNode('A');
const text2 = document.createTextNode('X');
const text3 = document.createTextNode('C');
container.appendChild(text1);
container.appendChild(text2);
container.appendChild(text3);
const replacement = document.createTextNode('B');
text2.replaceWith(replacement);
testing.expectEqual(3, container.childNodes.length);
testing.expectEqual('ABC', container.textContent);
testing.expectEqual(replacement, text1.nextSibling);
testing.expectEqual(text3, replacement.nextSibling);
}
</script>
<script id="replaceWithMixedTypes">
{
// replaceWith() with mixed node types
const container = $('#container');
container.innerHTML = '';
const old = document.createComment('old');
container.appendChild(old);
const elem = document.createElement('span');
elem.textContent = 'E';
const text = document.createTextNode('T');
const comment = document.createComment('C');
old.replaceWith(elem, text, comment);
testing.expectEqual(3, container.childNodes.length);
testing.expectEqual(1, container.childNodes[0].nodeType); // ELEMENT_NODE
testing.expectEqual(3, container.childNodes[1].nodeType); // TEXT_NODE
testing.expectEqual(8, container.childNodes[2].nodeType); // COMMENT_NODE
testing.expectEqual(null, old.parentNode);
}
</script>
<script id="nextElementSiblingText">
{
// nextElementSibling on text node with element siblings
const container = $('#container');
container.innerHTML = '';
const text1 = document.createTextNode('A');
const comment = document.createComment('comment');
const div = document.createElement('div');
div.id = 'found';
const text2 = document.createTextNode('B');
container.appendChild(text1);
container.appendChild(comment);
container.appendChild(div);
container.appendChild(text2);
testing.expectEqual('found', text1.nextElementSibling.id);
testing.expectEqual('found', comment.nextElementSibling.id);
testing.expectEqual(null, text2.nextElementSibling);
}
</script>
<script id="nextElementSiblingNoElement">
{
// nextElementSibling when there's no element sibling
const container = $('#container');
container.innerHTML = '';
const text = document.createTextNode('A');
const comment = document.createComment('B');
container.appendChild(text);
container.appendChild(comment);
testing.expectEqual(null, text.nextElementSibling);
testing.expectEqual(null, comment.nextElementSibling);
}
</script>
<script id="previousElementSiblingComment">
{
// previousElementSibling on comment with element siblings
const container = $('#container');
container.innerHTML = '';
const div = document.createElement('div');
div.id = 'found';
const text = document.createTextNode('text');
const comment = document.createComment('comment');
container.appendChild(div);
container.appendChild(text);
container.appendChild(comment);
testing.expectEqual('found', text.previousElementSibling.id);
testing.expectEqual('found', comment.previousElementSibling.id);
testing.expectEqual(null, div.previousElementSibling);
}
</script>
<script id="previousElementSiblingNoElement">
{
// previousElementSibling when there's no element sibling
const container = $('#container');
container.innerHTML = '';
const text = document.createTextNode('A');
const comment = document.createComment('B');
container.appendChild(text);
container.appendChild(comment);
testing.expectEqual(null, text.previousElementSibling);
testing.expectEqual(null, comment.previousElementSibling);
}
</script>

View File

@@ -0,0 +1,238 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="container"></div>
<script id="appendChildBasic">
{
// DocumentFragment children should be moved, fragment itself should not appear
const container = $('#container');
container.innerHTML = '';
const fragment = document.createDocumentFragment();
const span1 = document.createElement('span');
span1.textContent = 'A';
const span2 = document.createElement('span');
span2.textContent = 'B';
fragment.appendChild(span1);
fragment.appendChild(span2);
// Fragment should have 2 children before insertion
testing.expectEqual(2, fragment.childNodes.length);
container.appendChild(fragment);
// After insertion:
// 1. Container should have 2 children (the spans), not 1 (the fragment)
testing.expectEqual(2, container.childNodes.length);
testing.expectEqual('SPAN', container.childNodes[0].tagName);
testing.expectEqual('SPAN', container.childNodes[1].tagName);
testing.expectEqual('A', container.childNodes[0].textContent);
testing.expectEqual('B', container.childNodes[1].textContent);
// 2. Fragment should be empty after insertion
testing.expectEqual(0, fragment.childNodes.length);
// 3. No DocumentFragment should appear in the tree
testing.expectEqual('SPAN', container.firstChild.tagName);
testing.expectEqual('SPAN', container.firstChild.nodeName);
}
</script>
<script id="insertBeforeBasic">
{
// insertBefore with DocumentFragment
const container = $('#container');
container.innerHTML = '<div id="ref">REF</div>';
const fragment = document.createDocumentFragment();
const span1 = document.createElement('span');
span1.textContent = 'A';
const span2 = document.createElement('span');
span2.textContent = 'B';
fragment.appendChild(span1);
fragment.appendChild(span2);
const ref = $('#ref');
container.insertBefore(fragment, ref);
// Container should have: span1, span2, ref (3 children)
testing.expectEqual(3, container.childNodes.length);
testing.expectEqual('SPAN', container.childNodes[0].tagName);
testing.expectEqual('A', container.childNodes[0].textContent);
testing.expectEqual('SPAN', container.childNodes[1].tagName);
testing.expectEqual('B', container.childNodes[1].textContent);
testing.expectEqual('DIV', container.childNodes[2].tagName);
testing.expectEqual('REF', container.childNodes[2].textContent);
// Fragment should be empty
testing.expectEqual(0, fragment.childNodes.length);
}
</script>
<script id="insertBeforeNull">
{
// insertBefore with null ref node should behave like appendChild
const container = $('#container');
container.innerHTML = '';
const fragment = document.createDocumentFragment();
const span = document.createElement('span');
span.textContent = 'TEST';
fragment.appendChild(span);
container.insertBefore(fragment, null);
testing.expectEqual(1, container.childNodes.length);
testing.expectEqual('SPAN', container.childNodes[0].tagName);
testing.expectEqual(0, fragment.childNodes.length);
}
</script>
<script id="replaceChildWithFragment">
{
// replaceChild with DocumentFragment
const container = $('#container');
container.innerHTML = '<div id="old">OLD</div>';
const fragment = document.createDocumentFragment();
const span1 = document.createElement('span');
span1.textContent = 'A';
const span2 = document.createElement('span');
span2.textContent = 'B';
fragment.appendChild(span1);
fragment.appendChild(span2);
const old = $('#old');
container.replaceChild(fragment, old);
// Container should have 2 children (the fragment's children)
testing.expectEqual(2, container.childNodes.length);
testing.expectEqual('SPAN', container.childNodes[0].tagName);
testing.expectEqual('A', container.childNodes[0].textContent);
testing.expectEqual('SPAN', container.childNodes[1].tagName);
testing.expectEqual('B', container.childNodes[1].textContent);
// Old node should not be in container
testing.expectEqual(null, old.parentNode);
}
</script>
<script id="emptyFragment">
{
// Empty DocumentFragment should not add any nodes
const container = $('#container');
container.innerHTML = '<span>TEST</span>';
const fragment = document.createDocumentFragment();
container.appendChild(fragment);
// Should still have just 1 child
testing.expectEqual(1, container.childNodes.length);
testing.expectEqual('SPAN', container.childNodes[0].tagName);
}
</script>
<script id="fragmentReuse">
{
// DocumentFragment can be reused after its children are moved
const container = $('#container');
container.innerHTML = '';
const fragment = document.createDocumentFragment();
const span1 = document.createElement('span');
span1.textContent = 'A';
fragment.appendChild(span1);
container.appendChild(fragment);
testing.expectEqual(1, container.childNodes.length);
testing.expectEqual(0, fragment.childNodes.length);
// Reuse the same fragment
const span2 = document.createElement('span');
span2.textContent = 'B';
fragment.appendChild(span2);
container.appendChild(fragment);
testing.expectEqual(2, container.childNodes.length);
testing.expectEqual('A', container.childNodes[0].textContent);
testing.expectEqual('B', container.childNodes[1].textContent);
}
</script>
<script id="nestedFragments">
{
// DocumentFragment containing another DocumentFragment
// (though this is unusual, the inner fragment should also be unwrapped)
const container = $('#container');
container.innerHTML = '';
const outer = document.createDocumentFragment();
const inner = document.createDocumentFragment();
const span = document.createElement('span');
span.textContent = 'TEST';
inner.appendChild(span);
// Appending inner fragment to outer should move span to outer
outer.appendChild(inner);
testing.expectEqual(1, outer.childNodes.length);
testing.expectEqual('SPAN', outer.childNodes[0].tagName);
testing.expectEqual(0, inner.childNodes.length);
// Now append outer to container
container.appendChild(outer);
testing.expectEqual(1, container.childNodes.length);
testing.expectEqual('SPAN', container.childNodes[0].tagName);
testing.expectEqual(0, outer.childNodes.length);
}
</script>
<script id="fragmentWithMixedContent">
{
// DocumentFragment with text nodes, comments, and elements
const container = $('#container');
container.innerHTML = '';
const fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode('Text1'));
fragment.appendChild(document.createComment('comment'));
const div = document.createElement('div');
div.textContent = 'Div';
fragment.appendChild(div);
fragment.appendChild(document.createTextNode('Text2'));
testing.expectEqual(4, fragment.childNodes.length);
container.appendChild(fragment);
// All 4 nodes should be in container
testing.expectEqual(4, container.childNodes.length);
testing.expectEqual(3, container.childNodes[0].nodeType); // TEXT_NODE
testing.expectEqual(8, container.childNodes[1].nodeType); // COMMENT_NODE
testing.expectEqual(1, container.childNodes[2].nodeType); // ELEMENT_NODE
testing.expectEqual(3, container.childNodes[3].nodeType); // TEXT_NODE
testing.expectEqual(0, fragment.childNodes.length);
}
</script>
<script id="innerHTML">
{
// After a DocumentFragment is inserted, innerHTML should not show it
const container = $('#container');
container.innerHTML = '';
const fragment = document.createDocumentFragment();
const comment = document.createComment('test');
fragment.appendChild(comment);
container.appendChild(fragment);
// Should only see the comment, not a DocumentFragment wrapper
const html = container.innerHTML;
testing.expectEqual('<!--test-->', html);
}
</script>

View File

@@ -45,6 +45,11 @@
const req = new Request('https://example.com/api', { headers });
testing.expectEqual('value', req.headers.get('X-Custom'));
}
{
const req = new Request('https://example.com/api', {headers: {over: '9000!'}});
testing.expectEqual('9000!', req.headers.get('over'));
}
</script>
<script id=request_input>
@@ -102,3 +107,4 @@
testing.expectEqual('https://example.com/custom', req.url);
}
</script>

View File

@@ -79,6 +79,119 @@ pub fn format(self: *const CData, writer: *std.io.Writer) !void {
};
}
pub fn getLength(self: *const CData) usize {
return self._data.len;
}
pub fn appendData(self: *CData, data: []const u8, page: *Page) !void {
const new_data = try std.mem.concat(page.arena, u8, &.{ self._data, data });
try self.setData(new_data, page);
}
pub fn deleteData(self: *CData, offset: usize, count: usize, page: *Page) !void {
if (offset > self._data.len) return error.IndexSizeError;
const end = @min(offset + count, self._data.len);
// Just slice - original data stays in arena
const old_value = self._data;
if (offset == 0) {
self._data = self._data[end..];
} else if (end >= self._data.len) {
self._data = self._data[0..offset];
} else {
self._data = try std.mem.concat(page.arena, u8, &.{
self._data[0..offset],
self._data[end..],
});
}
page.characterDataChange(self.asNode(), old_value);
}
pub fn insertData(self: *CData, offset: usize, data: []const u8, page: *Page) !void {
if (offset > self._data.len) return error.IndexSizeError;
const new_data = try std.mem.concat(page.arena, u8, &.{
self._data[0..offset],
data,
self._data[offset..],
});
try self.setData(new_data, page);
}
pub fn replaceData(self: *CData, offset: usize, count: usize, data: []const u8, page: *Page) !void {
if (offset > self._data.len) return error.IndexSizeError;
const end = @min(offset + count, self._data.len);
const new_data = try std.mem.concat(page.arena, u8, &.{
self._data[0..offset],
data,
self._data[end..],
});
try self.setData(new_data, page);
}
pub fn substringData(self: *const CData, offset: usize, count: usize) ![]const u8 {
if (offset > self._data.len) return error.IndexSizeError;
const end = @min(offset + count, self._data.len);
return self._data[offset..end];
}
pub fn remove(self: *CData, page: *Page) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
_ = try parent.removeChild(node, page);
}
pub fn before(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.insertBefore(child, node, page);
}
}
pub fn after(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
const next = node.nextSibling();
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.insertBefore(child, next, page);
}
}
pub fn replaceWith(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void {
const node = self.asNode();
const parent = node.parentNode() orelse return;
const next = node.nextSibling();
_ = try parent.removeChild(node, page);
for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page);
_ = try parent.insertBefore(child, next, page);
}
}
pub fn nextElementSibling(self: *CData) ?*Node.Element {
var maybe_sibling = self.asNode().nextSibling();
while (maybe_sibling) |sibling| {
if (sibling.is(Node.Element)) |el| return el;
maybe_sibling = sibling.nextSibling();
}
return null;
}
pub fn previousElementSibling(self: *CData) ?*Node.Element {
var maybe_sibling = self.asNode().previousSibling();
while (maybe_sibling) |sibling| {
if (sibling.is(Node.Element)) |el| return el;
maybe_sibling = sibling.previousSibling();
}
return null;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(CData);
@@ -89,4 +202,24 @@ pub const JsApi = struct {
};
pub const data = bridge.accessor(CData.getData, CData.setData, .{});
pub const length = bridge.accessor(CData.getLength, null, .{});
pub const appendData = bridge.function(CData.appendData, .{});
pub const deleteData = bridge.function(CData.deleteData, .{ .dom_exception = true });
pub const insertData = bridge.function(CData.insertData, .{ .dom_exception = true });
pub const replaceData = bridge.function(CData.replaceData, .{ .dom_exception = true });
pub const substringData = bridge.function(CData.substringData, .{ .dom_exception = true });
pub const remove = bridge.function(CData.remove, .{});
pub const before = bridge.function(CData.before, .{});
pub const after = bridge.function(CData.after, .{});
pub const replaceWith = bridge.function(CData.replaceWith, .{});
pub const nextElementSibling = bridge.accessor(CData.nextElementSibling, null, .{});
pub const previousElementSibling = bridge.accessor(CData.previousElementSibling, null, .{});
};
const testing = @import("../../testing.zig");
test "WebApi: CData" {
try testing.htmlRunner("cdata", .{});
}

View File

@@ -33,6 +33,7 @@ pub fn fromError(err: anyerror) ?DOMException {
error.NotFound => .{ ._code = .not_found },
error.NotSupported => .{ ._code = .not_supported },
error.HierarchyError => .{ ._code = .hierarchy_error },
error.IndexSizeError => .{ ._code = .index_size_error },
else => null,
};
}
@@ -45,6 +46,7 @@ pub fn getName(self: *const DOMException) []const u8 {
return switch (self._code) {
.none => "Error",
.invalid_character_error => "InvalidCharacterError",
.index_size_error => "IndexSizeErorr",
.syntax_error => "SyntaxError",
.not_found => "NotFoundError",
.not_supported => "NotSupportedError",
@@ -56,6 +58,7 @@ pub fn getMessage(self: *const DOMException) []const u8 {
return switch (self._code) {
.none => "",
.invalid_character_error => "Invalid Character",
.index_size_error => "IndexSizeError: Index or size is negative or greater than the allowed amount",
.syntax_error => "Syntax Error",
.not_supported => "Not Supported",
.not_found => "Not Found",
@@ -65,6 +68,7 @@ pub fn getMessage(self: *const DOMException) []const u8 {
const Code = enum(u8) {
none = 0,
index_size_error = 1,
hierarchy_error = 3,
invalid_character_error = 5,
not_found = 8,

View File

@@ -26,17 +26,18 @@ const Page = @import("../Page.zig");
const reflect = @import("../reflect.zig");
const Node = @import("Node.zig");
const CSS = @import("CSS.zig");
const DOMRect = @import("DOMRect.zig");
const ShadowRoot = @import("ShadowRoot.zig");
const collections = @import("collections.zig");
const Selector = @import("selector/Selector.zig");
pub const Attribute = @import("element/Attribute.zig");
const Animation = @import("animation/Animation.zig");
const DOMStringMap = @import("element/DOMStringMap.zig");
const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
pub const DOMStringMap = @import("element/DOMStringMap.zig");
const DOMRect = @import("DOMRect.zig");
const CSS = @import("CSS.zig");
const ShadowRoot = @import("ShadowRoot.zig");
pub const Svg = @import("element/Svg.zig");
pub const Html = @import("element/Html.zig");
pub const Attribute = @import("element/Attribute.zig");
const Element = @This();
@@ -587,6 +588,14 @@ pub fn querySelectorAll(self: *Element, input: []const u8, page: *Page) !*Select
return Selector.querySelectorAll(self.asNode(), input, page);
}
pub fn getAnimations(_: *const Element) []*Animation {
return &.{};
}
pub fn animate(_: *Element, _: js.Object, _: js.Object) !Animation {
return Animation.init();
}
pub fn closest(self: *Element, selector: []const u8, page: *Page) !?*Element {
if (selector.len == 0) {
return error.SyntaxError;
@@ -1012,6 +1021,8 @@ pub const JsApi = struct {
pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true });
pub const closest = bridge.function(Element.closest, .{ .dom_exception = true });
pub const getAnimations = bridge.function(Element.getAnimations, .{});
pub const animate = bridge.function(Element.animate, .{});
pub const checkVisibility = bridge.function(Element.checkVisibility, .{});
pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});

View File

@@ -41,9 +41,40 @@ pub const empty: KeyValueList = .{
._entries = .empty,
};
pub fn copy(arena: Allocator, original: KeyValueList) !KeyValueList {
var list = KeyValueList.init();
try list.ensureTotalCapacity(arena, original.len());
for (original._entries.items) |entry| {
try list.appendAssumeCapacity(arena, entry.name.str(), entry.value.str());
}
return list;
}
pub fn fromJsObject(arena: Allocator, js_obj: js.Object) !KeyValueList {
var it = js_obj.nameIterator();
var list = KeyValueList.init();
try list.ensureTotalCapacity(arena, it.count);
while (try it.next()) |name| {
const js_value = try js_obj.get(name);
const value = try js_value.toString(arena);
try list._entries.append(arena, .{
.name = try String.init(arena, name, .{}),
.value = try String.init(arena, value, .{}),
});
}
return list;
}
pub const Entry = struct {
name: String,
value: String,
pub fn format(self: Entry, writer: *std.Io.Writer) !void {
return writer.print("{f}: {f}", .{ self.name, self.value });
}
};
pub fn init() KeyValueList {

View File

@@ -143,7 +143,6 @@ pub fn parentElement(self: *const Node) ?*Element {
}
pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {
// Special case: DocumentFragment - append all its children instead
if (child.is(DocumentFragment)) |_| {
try page.appendAllChildren(child, self);
return child;
@@ -338,6 +337,11 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page
return error.NotFound;
}
if (new_node.is(DocumentFragment)) |_| {
try page.insertAllChildrenBefore(new_node, self, ref_node);
return new_node;
}
const child_already_connected = new_node.isConnected();
page.domChanged();

View File

@@ -157,7 +157,7 @@ pub fn setOnUnhandledRejection(self: *Window, cb_: ?js.Function) !void {
}
}
pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.RequestInit, page: *Page) !js.Promise {
pub fn fetch(_: *const Window, input: Fetch.Input, options: ?Fetch.InitOpts, page: *Page) !js.Promise {
return Fetch.init(input, options, page);
}

View File

@@ -0,0 +1,49 @@
// 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 js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Animation = @This();
pub fn init() !Animation {
return .{};
}
pub fn play(_: *Animation) void {}
pub fn pause(_: *Animation) void {}
pub fn cancel(_: *Animation) void {}
pub fn finish(_: *Animation) void {}
pub fn reverse(_: *Animation) void {}
pub const JsApi = struct {
pub const bridge = js.Bridge(Animation);
pub const Meta = struct {
pub const name = "Animation";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const empty_with_no_proto = true;
};
pub const play = bridge.function(Animation.play, .{});
pub const pause = bridge.function(Animation.pause, .{});
pub const cancel = bridge.function(Animation.cancel, .{});
pub const finish = bridge.function(Animation.finish, .{});
pub const reverse = bridge.function(Animation.reverse, .{});
};

View File

@@ -40,10 +40,10 @@ _response: *Response,
_resolver: js.PersistentPromiseResolver,
pub const Input = Request.Input;
pub const RequestInit = Request.Options;
pub const InitOpts = Request.InitOpts;
// @ZIGDOM just enough to get campfire demo working
pub fn init(input: Input, options: ?RequestInit, page: *Page) !js.Promise {
pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
const request = try Request.init(input, options, page);
const fetch = try page.arena.create(Fetch);
@@ -56,7 +56,11 @@ pub fn init(input: Input, options: ?RequestInit, page: *Page) !js.Promise {
};
const http_client = page._session.browser.http_client;
const headers = try http_client.newHeaders();
var headers = try http_client.newHeaders();
if (request._headers) |h| {
try h.populateHttpHeader(page.call_arena, &headers);
}
try page.requestCookie(.{}).headersForRequest(page.arena, request._url, &headers);
if (comptime IS_DEBUG) {
log.debug(.http, "fetch", .{ .url = request._url });

View File

@@ -5,16 +5,34 @@ const log = @import("../../../log.zig");
const Page = @import("../../Page.zig");
const KeyValueList = @import("../KeyValueList.zig");
const Allocator = std.mem.Allocator;
const Headers = @This();
_list: KeyValueList,
pub fn init(page: *Page) !*Headers {
pub const InitOpts = union(enum) {
obj: *Headers,
js_obj: js.Object,
};
pub fn init(opts_: ?InitOpts, page: *Page) !*Headers {
const list = if (opts_) |opts| switch (opts) {
.obj => |obj| try KeyValueList.copy(page.arena, obj._list),
.js_obj => |js_obj| try KeyValueList.fromJsObject(page.arena, js_obj),
} else KeyValueList.init();
return page._factory.create(Headers{
._list = KeyValueList.init(),
._list = list,
});
}
// pub fn fromJsObject(js_obj: js.Object, page: *Page) !*Headers {
// return page._factory.create(Headers{
// ._list = try KeyValueList.fromJsObject(page.arena, js_obj),
// });
// }
pub fn append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
const normalized_name = normalizeHeaderName(name, page);
try self._list.append(page.arena, normalized_name, value);
@@ -63,6 +81,15 @@ pub fn forEach(self: *Headers, cb_: js.Function, js_this_: ?js.Object) !void {
}
}
// TODO: do we really need 2 different header structs??
const Http = @import("../../../http/Http.zig");
pub fn populateHttpHeader(self: *Headers, allocator: Allocator, http_headers: *Http.Headers) !void {
for (self._list._entries.items) |entry| {
const merged = try std.mem.concatWithSentinel(allocator, u8, &.{ entry.name.str(), ": ", entry.value.str() }, 0);
try http_headers.add(merged);
}
}
fn normalizeHeaderName(name: []const u8, page: *Page) []const u8 {
if (name.len > page.buf.len) {
return name;

View File

@@ -37,19 +37,19 @@ pub const Input = union(enum) {
url: [:0]const u8,
};
pub const Options = struct {
pub const InitOpts = struct {
method: ?[]const u8 = null,
headers: ?*Headers = null,
headers: ?Headers.InitOpts = null,
};
pub fn init(input: Input, opts_: ?Options, page: *Page) !*Request {
pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request {
const arena = page.arena;
const url = switch (input) {
.url => |u| try URL.resolve(arena, page.url, u, .{ .always_dupe = true }),
.request => |r| try arena.dupeZ(u8, r._url),
};
const opts = opts_ orelse Options{};
const opts = opts_ orelse InitOpts{};
const method = if (opts.method) |m|
try parseMethod(m, page)
else switch (input) {
@@ -57,8 +57,8 @@ pub fn init(input: Input, opts_: ?Options, page: *Page) !*Request {
.request => |r| r._method,
};
const headers = if (opts.headers) |h|
h
const headers = if (opts.headers) |header_init|
try Headers.init(header_init, page)
else switch (input) {
.url => null,
.request => |r| r._headers,
@@ -103,7 +103,7 @@ pub fn getHeaders(self: *Request, page: *Page) !*Headers {
return headers;
}
const headers = try Headers.init(page);
const headers = try Headers.init(null, page);
self._headers = headers;
return headers;
}

View File

@@ -56,7 +56,7 @@ pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response {
._arena = page.arena,
._status = opts.status,
._body = body,
._headers = opts.headers orelse try Headers.init(page),
._headers = opts.headers orelse try Headers.init(null, page),
._type = .basic, // @ZIGDOM: todo
});
}

View File

@@ -45,7 +45,7 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {
.query_string => |qs| break :blk try paramsFromString(arena, qs, &page.buf),
.value => |js_val| {
if (js_val.isObject()) {
break :blk try paramsFromObject(arena, js_val.toObject());
break :blk try KeyValueList.fromJsObject(arena, js_val.toObject());
}
if (js_val.isString()) {
break :blk try paramsFromString(arena, try js_val.toString(arena), &page.buf);
@@ -187,25 +187,6 @@ fn paramsFromString(allocator: Allocator, input_: []const u8, buf: []u8) !KeyVal
return params;
}
fn paramsFromObject(arena: Allocator, js_obj: js.Object) !KeyValueList {
var it = js_obj.nameIterator(arena);
var params = KeyValueList.init();
try params.ensureTotalCapacity(arena, it.count);
while (try it.next()) |name| {
const js_value = try js_obj.get(name);
const value = try js_value.toString(arena);
try params._entries.append(arena, .{
.name = try String.init(arena, name, .{}),
.value = try String.init(arena, value, .{}),
});
}
return params;
}
fn unescape(arena: Allocator, value: []const u8, buf: []u8) !String {
if (value.len == 0) {
return String.init(undefined, "", .{});

View File

@@ -26,6 +26,7 @@ const URL = @import("../../URL.zig");
const Mime = @import("../../Mime.zig");
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const Headers = @import("Headers.zig");
const EventTarget = @import("../EventTarget.zig");
const XMLHttpRequestEventTarget = @import("XMLHttpRequestEventTarget.zig");
@@ -40,6 +41,7 @@ _transfer: ?*Http.Transfer = null,
_url: [:0]const u8 = "",
_method: Http.Method = .GET,
_request_headers: *Headers,
_request_body: ?[]const u8 = null,
_response: std.ArrayList(u8) = .empty,
@@ -71,6 +73,7 @@ pub fn init(page: *Page) !*XMLHttpRequest {
._page = page,
._proto = undefined,
._arena = page.arena,
._request_headers = try Headers.init(null, page),
});
}
@@ -129,6 +132,10 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void
try self.stateChanged(.opened, self._page);
}
pub fn setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8, page: *Page) !void {
return self._request_headers.append(name, value, page);
}
pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
if (comptime IS_DEBUG) {
log.debug(.http, "XMLHttpRequest.send", .{ .url = self._url });
@@ -143,10 +150,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
const page = self._page;
const http_client = page._session.browser.http_client;
var headers = try http_client.newHeaders();
// @ZIGDOM
// for (self._headers.items) |hdr| {
// try headers.add(hdr);
// }
try self._request_headers.populateHttpHeader(page.call_arena, &headers);
try page.requestCookie(.{}).headersForRequest(self._arena, self._url, &headers);
try http_client.request(.{
@@ -351,6 +355,7 @@ pub const JsApi = struct {
pub const responseType = bridge.accessor(XMLHttpRequest.getResponseType, XMLHttpRequest.setResponseType, .{});
pub const status = bridge.accessor(XMLHttpRequest.getStatus, null, .{});
pub const response = bridge.accessor(XMLHttpRequest.getResponse, null, .{});
pub const setRequestHeader = bridge.function(XMLHttpRequest.setRequestHeader, .{});
};
const testing = @import("../../../testing.zig");