mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-16 16:28:58 +00:00
Properly handle insertion of DocumentFragment
Add various CData methods XHR and Fetch request headers Animation mocks
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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, .{});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"),
|
||||
|
||||
730
src/browser/tests/cdata/character_data.html
Normal file
730
src/browser/tests/cdata/character_data.html
Normal 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>
|
||||
238
src/browser/tests/document_fragment/insertion.html
Normal file
238
src/browser/tests/document_fragment/insertion.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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", .{});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, .{});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
49
src/browser/webapi/animation/Animation.zig
Normal file
49
src/browser/webapi/animation/Animation.zig
Normal 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, .{});
|
||||
};
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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, "", .{});
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user