1 Commits

Author SHA1 Message Date
Muki Kiboigo
b6e8aff2c9 ensure that records persist in arena 2026-01-05 09:15:15 -08:00
60 changed files with 344 additions and 2281 deletions

View File

@@ -13,7 +13,7 @@ inputs:
zig-v8: zig-v8:
description: 'zig v8 version to install' description: 'zig v8 version to install'
required: false required: false
default: 'v0.2.2' default: 'v0.1.37'
v8: v8:
description: 'v8 version to install' description: 'v8 version to install'
required: false required: false

View File

@@ -5,12 +5,8 @@ env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }} AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }} AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
on: on:
push:
tags:
- '*'
schedule: schedule:
- cron: "2 2 * * *" - cron: "2 2 * * *"
@@ -42,11 +38,8 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
mode: 'release' mode: 'release'
- name: v8 snapshot
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
- name: zig build - name: zig build
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -61,7 +54,7 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }} tag: nightly
build-linux-aarch64: build-linux-aarch64:
env: env:
@@ -84,11 +77,8 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
mode: 'release' mode: 'release'
- name: v8 snapshot
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
- name: zig build - name: zig build
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -103,7 +93,7 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }} tag: nightly
build-macos-aarch64: build-macos-aarch64:
env: env:
@@ -128,11 +118,8 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
mode: 'release' mode: 'release'
- name: v8 snapshot
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
- name: zig build - name: zig build
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -147,7 +134,7 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }} tag: nightly
build-macos-x86_64: build-macos-x86_64:
env: env:
@@ -170,11 +157,8 @@ jobs:
arch: ${{env.ARCH}} arch: ${{env.ARCH}}
mode: 'release' mode: 'release'
- name: v8 snapshot
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
- name: zig build - name: zig build
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseSafe -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
- name: Rename binary - name: Rename binary
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
@@ -189,4 +173,4 @@ jobs:
with: with:
allowUpdates: true allowUpdates: true
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }} artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
tag: ${{ env.RELEASE }} tag: nightly

View File

@@ -3,7 +3,7 @@ FROM debian:stable-slim
ARG MINISIG=0.12 ARG MINISIG=0.12
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=14.0.365.4 ARG V8=14.0.365.4
ARG ZIG_V8=v0.2.2 ARG ZIG_V8=v0.1.37
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN apt-get update -yq && \ RUN apt-get update -yq && \
@@ -48,16 +48,8 @@ RUN case $TARGETPLATFORM in \
mkdir -p v8/ && \ mkdir -p v8/ && \
mv libc_v8.a v8/libc_v8.a mv libc_v8.a v8/libc_v8.a
# build v8 snapshot
RUN zig build -Doptimize=ReleaseFast \
-Dprebuilt_v8_path=v8/libc_v8.a \
snapshot_creator -- src/snapshot.bin
# build release # build release
RUN zig build -Doptimize=ReleaseFast \ RUN zig build -Doptimize=ReleaseFast -Dprebuilt_v8_path=v8/libc_v8.a -Dgit_commit=$(git rev-parse --short HEAD)
-Dsnapshot_path=../../snapshot.bin \
-Dprebuilt_v8_path=v8/libc_v8.a \
-Dgit_commit=$(git rev-parse --short HEAD)
FROM debian:stable-slim FROM debian:stable-slim

View File

@@ -47,18 +47,12 @@ help:
# $(ZIG) commands # $(ZIG) commands
# ------------ # ------------
.PHONY: build build-v8-snapshot build-dev run run-release shell test bench wpt data end2end .PHONY: build build-dev run run-release shell test bench wpt data end2end
## Build v8 snapshot ## Build in release-safe mode
build-v8-snapshot: build:
@printf "\033[36mBuilding v8 snapshot (release safe)...\033[0m\n"
@$(ZIG) build -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
@printf "\033[33mBuild OK\033[0m\n"
## Build in release-fast mode
build: build-v8-snapshot
@printf "\033[36mBuilding (release safe)...\033[0m\n" @printf "\033[36mBuilding (release safe)...\033[0m\n"
@$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;) @$(ZIG) build -Doptimize=ReleaseSafe -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
@printf "\033[33mBuild OK\033[0m\n" @printf "\033[33mBuild OK\033[0m\n"
## Build in debug mode ## Build in debug mode

View File

@@ -211,23 +211,6 @@ env.
But you can directly use the zig command: `zig build run`. But you can directly use the zig command: `zig build run`.
#### Embed v8 snapshot
Lighpanda uses v8 snapshot. By default, it is created on startup but you can
embed it by using the following commands:
Generate the snapshot.
```
zig build snapshot_creator -- src/snapshot.bin
```
Build using the snapshot binary.
```
zig build -Dsnapshot_path=../../snapshot.bin
```
See [#1279](https://github.com/lightpanda-io/browser/pull/1279) for more details.
## Test ## Test
### Unit Tests ### Unit Tests

View File

@@ -6,8 +6,8 @@
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.15.2",
.dependencies = .{ .dependencies = .{
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d6b5f89cfc7feece29359e8c848bb916e8ecfab6.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/0d64a3d5b36ac94067df3e13fddbf715caa6f391.tar.gz",
.hash = "v8-0.0.0-xddH6_0gBABrJc5cL6-P2wGvvweTTCgWdpmClr9r-C-s", .hash = "v8-0.0.0-xddH65sfBAC8o3q41YxhOms5uY2fvMzBrsgN8IeCXZgE",
}, },
//.v8 = .{ .path = "../zig-v8-fork" }, //.v8 = .{ .path = "../zig-v8-fork" },
.@"boringssl-zig" = .{ .@"boringssl-zig" = .{

View File

@@ -40,8 +40,7 @@ arena: Allocator,
listener_pool: std.heap.MemoryPool(Listener), listener_pool: std.heap.MemoryPool(Listener),
list_pool: std.heap.MemoryPool(std.DoublyLinkedList), list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
lookup: std.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList), lookup: std.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList),
dispatch_depth: usize, dispatch_depth: u32 = 0,
deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),
pub fn init(page: *Page) EventManager { pub fn init(page: *Page) EventManager {
return .{ return .{
@@ -51,7 +50,6 @@ pub fn init(page: *Page) EventManager {
.list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena), .list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena),
.listener_pool = std.heap.MemoryPool(Listener).init(page.arena), .listener_pool = std.heap.MemoryPool(Listener).init(page.arena),
.dispatch_depth = 0, .dispatch_depth = 0,
.deferred_removals = .{},
}; };
} }
@@ -121,6 +119,9 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
} }
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void { pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
if (comptime IS_DEBUG) {
log.debug(.event, "eventManager.remove", .{ .type = typ, .capture = use_capture, .target = target });
}
const list = self.lookup.get(@intFromPtr(target)) orelse return; const list = self.lookup.get(@intFromPtr(target)) orelse return;
if (findListener(list, typ, callback, use_capture)) |listener| { if (findListener(list, typ, callback, use_capture)) |listener| {
self.removeListener(list, listener); self.removeListener(list, listener);
@@ -185,7 +186,7 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
if (function_) |func| { if (function_) |func| {
event._current_target = target; event._current_target = target;
if (func.callWithThis(void, target, .{event})) { if (func.call(void, .{event})) {
was_dispatched = true; was_dispatched = true;
} else |err| { } else |err| {
// a non-JS error // a non-JS error
@@ -298,53 +299,38 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
const page = self.page; const page = self.page;
const typ = event._type_string; const typ = event._type_string;
// Track dispatch depth for deferred removal // Track that we're dispatching to prevent immediate removal
self.dispatch_depth += 1; self.dispatch_depth += 1;
defer { defer {
const dispatch_depth = self.dispatch_depth; self.dispatch_depth -= 1;
// Only destroy deferred listeners when we exit the outermost dispatch // Clean up any marked listeners in this target's list after this phase
if (dispatch_depth == 1) { // We do this regardless of depth to handle cross-target removals correctly
for (self.deferred_removals.items) |removal| { self.cleanupMarkedListeners(list);
removal.list.remove(&removal.listener.node);
self.listener_pool.destroy(removal.listener);
}
self.deferred_removals.clearRetainingCapacity();
} else {
self.dispatch_depth = dispatch_depth - 1;
}
} }
// Use the last listener in the list as sentinel - listeners added during dispatch will be after it
const last_node = list.last orelse return;
const last_listener: *Listener = @alignCast(@fieldParentPtr("node", last_node));
// Iterate through the list, stopping after we've encountered the last_listener
var node = list.first; var node = list.first;
var is_done = false;
while (node) |n| { while (node) |n| {
if (is_done) { // do this now, in case we need to remove n (once: true or aborted signal)
break;
}
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
is_done = (listener == last_listener);
node = n.next; node = n.next;
// Skip non-matching listeners const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
// Skip listeners that were marked for removal
if (listener.marked_for_removal) {
continue;
}
if (!listener.typ.eql(typ)) { if (!listener.typ.eql(typ)) {
continue; continue;
} }
// Can be null when dispatching to the target itself
if (comptime capture_only) |capture| { if (comptime capture_only) |capture| {
if (listener.capture != capture) { if (listener.capture != capture) {
continue; continue;
} }
} }
// Skip removed listeners
if (listener.removed) {
continue;
}
// If the listener has an aborted signal, remove it and skip // If the listener has an aborted signal, remove it and skip
if (listener.signal) |signal| { if (listener.signal) |signal| {
if (signal.getAborted()) { if (signal.getAborted()) {
@@ -353,11 +339,6 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
} }
} }
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
if (listener.once) {
self.removeListener(list, listener);
}
was_handled.* = true; was_handled.* = true;
event._current_target = current_target; event._current_target = current_target;
@@ -368,7 +349,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
} }
switch (listener.function) { switch (listener.function) {
.value => |value| try value.callWithThis(void, current_target, .{event}), .value => |value| try value.call(void, .{event}),
.string => |string| { .string => |string| {
const str = try page.call_arena.dupeZ(u8, string.str()); const str = try page.call_arena.dupeZ(u8, string.str());
try self.page.js.eval(str, null); try self.page.js.eval(str, null);
@@ -385,6 +366,10 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
event._target = original_target; event._target = original_target;
} }
if (listener.once) {
self.removeListener(list, listener);
}
if (event._stop_immediate_propagation) { if (event._stop_immediate_propagation) {
return; return;
} }
@@ -397,17 +382,29 @@ fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target:
} }
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void { fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {
// If we're in a dispatch, defer removal to avoid invalidating iteration
if (self.dispatch_depth > 0) { if (self.dispatch_depth > 0) {
listener.removed = true; // We're in the middle of dispatching, just mark for removal
self.deferred_removals.append(self.arena, .{ .list = list, .listener = listener }) catch unreachable; // This prevents invalidating the linked list during iteration
listener.marked_for_removal = true;
} else { } else {
// Outside dispatch, remove immediately // Safe to remove immediately
list.remove(&listener.node); list.remove(&listener.node);
self.listener_pool.destroy(listener); self.listener_pool.destroy(listener);
} }
} }
fn cleanupMarkedListeners(self: *EventManager, list: *std.DoublyLinkedList) void {
var node = list.first;
while (node) |n| {
node = n.next;
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
if (listener.marked_for_removal) {
list.remove(&listener.node);
self.listener_pool.destroy(listener);
}
}
}
fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Callback, capture: bool) ?*Listener { fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Callback, capture: bool) ?*Listener {
var node = list.first; var node = list.first;
while (node) |n| { while (node) |n| {
@@ -439,7 +436,7 @@ const Listener = struct {
function: Function, function: Function,
signal: ?*@import("webapi/AbortSignal.zig") = null, signal: ?*@import("webapi/AbortSignal.zig") = null,
node: std.DoublyLinkedList.Node, node: std.DoublyLinkedList.Node,
removed: bool = false, marked_for_removal: bool = false,
}; };
const Function = union(enum) { const Function = union(enum) {

View File

@@ -168,18 +168,6 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
return chain.get(1); return chain.get(1);
} }
fn eventInit(typ: []const u8, value: anytype, page: *Page) !Event {
// Round to 2ms for privacy (browsers do this)
const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic);
const time_stamp = (raw_timestamp / 2) * 2;
return .{
._type = unionInit(Event.Type, value),
._type_string = try String.init(page.arena, typ, .{}),
._time_stamp = time_stamp,
};
}
// this is a root object // this is a root object
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) { pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
const allocator = self._slab.allocator(); const allocator = self._slab.allocator();
@@ -190,7 +178,10 @@ pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
// Special case: Event has a _type_string field, so we need manual setup // Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0); const event_ptr = chain.get(0);
event_ptr.* = try eventInit(typ, chain.get(1), self._page); event_ptr.* = .{
._type = unionInit(Event.Type, chain.get(1)),
._type_string = try String.init(self._page.arena, typ, .{}),
};
chain.setLeaf(1, child); chain.setLeaf(1, child);
return chain.get(1); return chain.get(1);
@@ -205,7 +196,10 @@ pub fn uiEvent(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child)
// Special case: Event has a _type_string field, so we need manual setup // Special case: Event has a _type_string field, so we need manual setup
const event_ptr = chain.get(0); const event_ptr = chain.get(0);
event_ptr.* = try eventInit(typ, chain.get(1), self._page); event_ptr.* = .{
._type = unionInit(Event.Type, chain.get(1)),
._type_string = try String.init(self._page.arena, typ, .{}),
};
chain.setMiddle(1, UIEvent.Type); chain.setMiddle(1, UIEvent.Type);
chain.setLeaf(2, child); chain.setLeaf(2, child);

View File

@@ -93,9 +93,7 @@ _attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attri
_element_styles: Element.StyleLookup = .{}, _element_styles: Element.StyleLookup = .{},
_element_datasets: Element.DatasetLookup = .{}, _element_datasets: Element.DatasetLookup = .{},
_element_class_lists: Element.ClassListLookup = .{}, _element_class_lists: Element.ClassListLookup = .{},
_element_rel_lists: Element.RelListLookup = .{},
_element_shadow_roots: Element.ShadowRootLookup = .{}, _element_shadow_roots: Element.ShadowRootLookup = .{},
_node_owner_documents: Node.OwnerDocumentLookup = .{},
_element_assigned_slots: Element.AssignedSlotLookup = .{}, _element_assigned_slots: Element.AssignedSlotLookup = .{},
_script_manager: ScriptManager, _script_manager: ScriptManager,
@@ -265,9 +263,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._element_styles = .{}; self._element_styles = .{};
self._element_datasets = .{}; self._element_datasets = .{};
self._element_class_lists = .{}; self._element_class_lists = .{};
self._element_rel_lists = .{};
self._element_shadow_roots = .{}; self._element_shadow_roots = .{};
self._node_owner_documents = .{};
self._element_assigned_slots = .{}; self._element_assigned_slots = .{};
self._notified_network_idle = .init; self._notified_network_idle = .init;
self._notified_network_almost_idle = .init; self._notified_network_almost_idle = .init;
@@ -997,32 +993,21 @@ pub fn domChanged(self: *Page) void {
}; };
} }
const ElementIdMaps = struct { lookup: *std.StringHashMapUnmanaged(*Element), removed_ids: *std.StringHashMapUnmanaged(void) }; fn getElementIdMap(page: *Page, node: *Node) *std.StringHashMapUnmanaged(*Element) {
fn getElementIdMap(page: *Page, node: *Node) ElementIdMaps {
// Walk up the tree checking for ShadowRoot and tracking the root // Walk up the tree checking for ShadowRoot and tracking the root
var current = node; var current = node;
while (true) { while (true) {
if (current.is(ShadowRoot)) |shadow_root| { if (current.is(ShadowRoot)) |shadow_root| {
return .{ return &shadow_root._elements_by_id;
.lookup = &shadow_root._elements_by_id,
.removed_ids = &shadow_root._removed_ids,
};
} }
const parent = current._parent orelse { const parent = current._parent orelse {
if (current._type == .document) { if (current._type == .document) {
return .{ return &current._type.document._elements_by_id;
.lookup = &current._type.document._elements_by_id,
.removed_ids = &current._type.document._removed_ids,
};
} }
// Detached nodes should not have IDs registered // Detached nodes should not have IDs registered
std.debug.assert(false); std.debug.assert(false);
return .{ return &page.document._elements_by_id;
.lookup = &page.document._elements_by_id,
.removed_ids = &page.document._removed_ids,
};
}; };
current = parent; current = parent;
@@ -1030,35 +1015,22 @@ fn getElementIdMap(page: *Page, node: *Node) ElementIdMaps {
} }
pub fn addElementId(self: *Page, parent: *Node, element: *Element, id: []const u8) !void { pub fn addElementId(self: *Page, parent: *Node, element: *Element, id: []const u8) !void {
var id_maps = self.getElementIdMap(parent); var id_map = self.getElementIdMap(parent);
const gop = try id_maps.lookup.getOrPut(self.arena, id); const gop = try id_map.getOrPut(self.arena, id);
if (!gop.found_existing) { if (!gop.found_existing) {
gop.value_ptr.* = element; gop.value_ptr.* = element;
return;
}
const existing = gop.value_ptr.*.asNode();
switch (element.asNode().compareDocumentPosition(existing)) {
0x04 => gop.value_ptr.* = element,
else => {},
} }
} }
pub fn removeElementId(self: *Page, element: *Element, id: []const u8) void { pub fn removeElementId(self: *Page, element: *Element, id: []const u8) void {
const node = element.asNode(); var id_map = self.getElementIdMap(element.asNode());
self.removeElementIdWithMaps(self.getElementIdMap(node), id); _ = id_map.remove(id);
}
pub fn removeElementIdWithMaps(self: *Page, id_maps: ElementIdMaps, id: []const u8) void {
if (id_maps.lookup.remove(id)) {
id_maps.removed_ids.put(self.arena, id, {}) catch {};
}
} }
pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Element { pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Element {
if (node.isConnected() or node.isInShadowTree()) { if (node.isConnected() or node.isInShadowTree()) {
const lookup = self.getElementIdMap(node).lookup; const id_map = self.getElementIdMap(node);
return lookup.get(id); return id_map.get(id);
} }
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(node, .{}); var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(node, .{});
while (tw.next()) |el| { while (tw.next()) |el| {
@@ -1315,26 +1287,6 @@ pub fn nodeComplete(self: *Page, node: *Node) !void {
return self.nodeIsReady(true, node); return self.nodeIsReady(true, node);
} }
// Sets the owner document for a node. Only stores entries for nodes whose owner
// is NOT page.document to minimize memory overhead.
pub fn setNodeOwnerDocument(self: *Page, node: *Node, owner: *Document) !void {
if (owner == self.document) {
// No need to store if it's the main document - remove if present
_ = self._node_owner_documents.remove(node);
} else {
try self._node_owner_documents.put(self.arena, node, owner);
}
}
// Recursively sets the owner document for a node and all its descendants
pub fn adoptNodeTree(self: *Page, node: *Node, new_owner: *Document) !void {
try self.setNodeOwnerDocument(node, new_owner);
var it = node.childrenIterator();
while (it.next()) |child| {
try self.adoptNodeTree(child, new_owner);
}
}
pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_iterator: anytype) !*Node { pub fn createElement(self: *Page, ns_: ?[]const u8, name: []const u8, attribute_iterator: anytype) !*Node {
const namespace: Element.Namespace = blk: { const namespace: Element.Namespace = blk: {
const ns = ns_ orelse break :blk .html; const ns = ns_ orelse break :blk .html;
@@ -2143,7 +2095,7 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts
// grab this before we null the parent // grab this before we null the parent
const was_connected = child.isConnected(); const was_connected = child.isConnected();
// Capture the ID map before disconnecting, so we can remove IDs from the correct document // Capture the ID map before disconnecting, so we can remove IDs from the correct document
const id_maps = if (was_connected) self.getElementIdMap(child) else null; const id_map = if (was_connected) self.getElementIdMap(child) else null;
child._parent = null; child._parent = null;
child._child_link = .{}; child._child_link = .{};
@@ -2194,7 +2146,7 @@ pub fn removeNode(self: *Page, parent: *Node, child: *Node, opts: RemoveNodeOpts
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{}); var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(child, .{});
while (tw.next()) |el| { while (tw.next()) |el| {
if (el.getAttributeSafe("id")) |id| { if (el.getAttributeSafe("id")) |id| {
self.removeElementIdWithMaps(id_maps.?, id); _ = id_map.?.remove(id);
} }
Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self); Element.Html.Custom.invokeDisconnectedCallbackOnElement(el, self);
@@ -2218,9 +2170,9 @@ pub fn appendAllChildren(self: *Page, parent: *Node, target: *Node) !void {
} }
} }
pub fn insertAllChildrenBefore(self: *Page, fragment: *Node, parent: *Node, ref_node: *Node) !void { pub fn insertAllChildrenBefore(self: *Page, fragment: *Node, target: *Node, ref_node: *Node) !void {
self.domChanged(); self.domChanged();
const dest_connected = parent.isConnected(); const dest_connected = target.isConnected();
var it = fragment.childrenIterator(); var it = fragment.childrenIterator();
while (it.next()) |child| { while (it.next()) |child| {
@@ -2228,7 +2180,7 @@ pub fn insertAllChildrenBefore(self: *Page, fragment: *Node, parent: *Node, ref_
const child_was_connected = child.isConnected(); const child_was_connected = child.isConnected();
self.removeNode(fragment, child, .{ .will_be_reconnected = dest_connected }); self.removeNode(fragment, child, .{ .will_be_reconnected = dest_connected });
try self.insertNodeRelative( try self.insertNodeRelative(
parent, target,
child, child,
.{ .before = ref_node }, .{ .before = ref_node },
.{ .child_already_connected = child_was_connected }, .{ .child_already_connected = child_was_connected },
@@ -2236,6 +2188,10 @@ pub fn insertAllChildrenBefore(self: *Page, fragment: *Node, parent: *Node, ref_
} }
} }
fn _appendNode(self: *Page, comptime from_parser: bool, parent: *Node, child: *Node, opts: InsertNodeOpts) !void {
self._insertNodeRelative(from_parser, parent, child, .append, opts);
}
const InsertNodeRelative = union(enum) { const InsertNodeRelative = union(enum) {
append, append,
after: *Node, after: *Node,

View File

@@ -239,17 +239,8 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
}; };
const is_blocking = script.mode == .normal; const is_blocking = script.mode == .normal;
if (is_blocking == false) {
self.scriptList(script).append(&script.node);
}
if (remote_url) |url| { if (remote_url) |url| {
errdefer { errdefer script.deinit(true);
if (is_blocking == false) {
self.scriptList(script).remove(&script.node);
}
script.deinit(true);
}
var headers = try self.client.newHeaders(); var headers = try self.client.newHeaders();
try page.requestCookie(.{}).headersForRequest(page.arena, url, &headers); try page.requestCookie(.{}).headersForRequest(page.arena, url, &headers);
@@ -280,6 +271,8 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
} }
if (is_blocking == false) { if (is_blocking == false) {
const list = self.scriptList(script);
list.append(&script.node);
return; return;
} }

View File

@@ -1,295 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Tokenizer = @import("Tokenizer.zig");
pub const Declaration = struct {
name: []const u8,
value: []const u8,
important: bool,
};
const TokenSpan = struct {
token: Tokenizer.Token,
start: usize,
end: usize,
};
const TokenStream = struct {
tokenizer: Tokenizer,
peeked: ?TokenSpan = null,
fn init(input: []const u8) TokenStream {
return .{ .tokenizer = .{ .input = input } };
}
fn nextRaw(self: *TokenStream) ?TokenSpan {
const start = self.tokenizer.position;
const token = self.tokenizer.next() orelse return null;
const end = self.tokenizer.position;
return .{ .token = token, .start = start, .end = end };
}
fn next(self: *TokenStream) ?TokenSpan {
if (self.peeked) |token| {
self.peeked = null;
return token;
}
return self.nextRaw();
}
fn peek(self: *TokenStream) ?TokenSpan {
if (self.peeked == null) {
self.peeked = self.nextRaw();
}
return self.peeked;
}
};
pub fn parseDeclarationsList(input: []const u8) DeclarationsIterator {
return DeclarationsIterator.init(input);
}
pub const DeclarationsIterator = struct {
input: []const u8,
stream: TokenStream,
pub fn init(input: []const u8) DeclarationsIterator {
return .{
.input = input,
.stream = TokenStream.init(input),
};
}
pub fn next(self: *DeclarationsIterator) ?Declaration {
while (true) {
self.skipTriviaAndSemicolons();
const peeked = self.stream.peek() orelse return null;
switch (peeked.token) {
.at_keyword => {
_ = self.stream.next();
self.skipAtRule();
},
.ident => |name| {
_ = self.stream.next();
if (self.consumeDeclaration(name)) |declaration| {
return declaration;
}
},
else => {
_ = self.stream.next();
self.skipInvalidDeclaration();
},
}
}
return null;
}
fn consumeDeclaration(self: *DeclarationsIterator, name: []const u8) ?Declaration {
self.skipTrivia();
const colon = self.stream.next() orelse return null;
if (!isColon(colon.token)) {
self.skipInvalidDeclaration();
return null;
}
const value = self.consumeValue() orelse return null;
return .{
.name = name,
.value = value.value,
.important = value.important,
};
}
const ValueResult = struct {
value: []const u8,
important: bool,
};
fn consumeValue(self: *DeclarationsIterator) ?ValueResult {
self.skipTrivia();
var depth: usize = 0;
var start: ?usize = null;
var last_sig: ?TokenSpan = null;
var prev_sig: ?TokenSpan = null;
while (true) {
const peeked = self.stream.peek() orelse break;
if (isSemicolon(peeked.token) and depth == 0) {
_ = self.stream.next();
break;
}
const span = self.stream.next() orelse break;
if (isWhitespaceOrComment(span.token)) {
continue;
}
if (start == null) start = span.start;
prev_sig = last_sig;
last_sig = span;
updateDepth(span.token, &depth);
}
const value_start = start orelse return null;
const last = last_sig orelse return null;
var important = false;
var end_pos = last.end;
if (isImportantPair(prev_sig, last)) {
important = true;
const bang = prev_sig orelse return null;
if (value_start >= bang.start) return null;
end_pos = bang.start;
}
var value_slice = self.input[value_start..end_pos];
value_slice = std.mem.trim(u8, value_slice, &std.ascii.whitespace);
if (value_slice.len == 0) return null;
return .{ .value = value_slice, .important = important };
}
fn skipTrivia(self: *DeclarationsIterator) void {
while (self.stream.peek()) |peeked| {
if (!isWhitespaceOrComment(peeked.token)) break;
_ = self.stream.next();
}
}
fn skipTriviaAndSemicolons(self: *DeclarationsIterator) void {
while (self.stream.peek()) |peeked| {
if (isWhitespaceOrComment(peeked.token) or isSemicolon(peeked.token)) {
_ = self.stream.next();
} else {
break;
}
}
}
fn skipAtRule(self: *DeclarationsIterator) void {
var depth: usize = 0;
var saw_block = false;
while (true) {
const peeked = self.stream.peek() orelse return;
if (!saw_block and isSemicolon(peeked.token) and depth == 0) {
_ = self.stream.next();
return;
}
const span = self.stream.next() orelse return;
if (isWhitespaceOrComment(span.token)) continue;
if (isBlockStart(span.token)) {
depth += 1;
saw_block = true;
} else if (isBlockEnd(span.token)) {
if (depth > 0) depth -= 1;
if (saw_block and depth == 0) return;
}
}
}
fn skipInvalidDeclaration(self: *DeclarationsIterator) void {
var depth: usize = 0;
while (self.stream.peek()) |peeked| {
if (isSemicolon(peeked.token) and depth == 0) {
_ = self.stream.next();
return;
}
const span = self.stream.next() orelse return;
if (isWhitespaceOrComment(span.token)) continue;
updateDepth(span.token, &depth);
}
}
};
fn isWhitespaceOrComment(token: Tokenizer.Token) bool {
return switch (token) {
.white_space, .comment => true,
else => false,
};
}
fn isSemicolon(token: Tokenizer.Token) bool {
return switch (token) {
.semicolon => true,
else => false,
};
}
fn isColon(token: Tokenizer.Token) bool {
return switch (token) {
.colon => true,
else => false,
};
}
fn isBlockStart(token: Tokenizer.Token) bool {
return switch (token) {
.curly_bracket_block, .square_bracket_block, .parenthesis_block, .function => true,
else => false,
};
}
fn isBlockEnd(token: Tokenizer.Token) bool {
return switch (token) {
.close_curly_bracket, .close_parenthesis, .close_square_bracket => true,
else => false,
};
}
fn updateDepth(token: Tokenizer.Token, depth: *usize) void {
if (isBlockStart(token)) {
depth.* += 1;
return;
}
if (isBlockEnd(token)) {
if (depth.* > 0) depth.* -= 1;
}
}
fn isImportantPair(prev_sig: ?TokenSpan, last_sig: TokenSpan) bool {
if (!isIdentImportant(last_sig.token)) return false;
const prev = prev_sig orelse return false;
return isBang(prev.token);
}
fn isIdentImportant(token: Tokenizer.Token) bool {
return switch (token) {
.ident => |name| std.ascii.eqlIgnoreCase(name, "important"),
else => false,
};
}
fn isBang(token: Tokenizer.Token) bool {
return switch (token) {
.delim => |c| c == '!',
else => false,
};
}

View File

@@ -644,10 +644,8 @@ fn consumeNumeric(self: *Tokenizer) Token {
fn consumeUnquotedUrl(self: *Tokenizer) ?Token { fn consumeUnquotedUrl(self: *Tokenizer) ?Token {
// TODO: true url parser // TODO: true url parser
if (self.nextByte()) |it| { if (self.nextByte()) |it| {
return self.consumeString(it == '\''); self.consumeString(it == '\'');
} }
return null;
} }
fn consumeIdentLike(self: *Tokenizer) Token { fn consumeIdentLike(self: *Tokenizer) Token {

View File

@@ -89,10 +89,6 @@ pub const CallOpts = struct {
}; };
pub fn constructor(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void { pub fn constructor(self: *Caller, comptime T: type, func: anytype, info: v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
if (!info.isConstructCall()) {
self.handleError(T, @TypeOf(func), error.InvalidArgument, info, opts);
return;
}
self._constructor(func, info) catch |err| { self._constructor(func, info) catch |err| {
self.handleError(T, @TypeOf(func), err, info, opts); self.handleError(T, @TypeOf(func), err, info, opts);
}; };

View File

@@ -203,6 +203,27 @@ fn trackCallback(self: *Context, pf: PersistentFunction) !void {
return self.callbacks.append(self.arena, pf); return self.callbacks.append(self.arena, pf);
} }
// Given an anytype, turns it into a v8.Object. The anytype could be:
// 1 - A V8.object already
// 2 - Our js.Object wrapper around a V8.Object
// 3 - A zig instance that has previously been given to V8
// (i.e., the value has to be known to the executor)
pub fn valueToExistingObject(self: *const Context, value: anytype) !v8.Object {
if (@TypeOf(value) == v8.Object) {
return value;
}
if (@TypeOf(value) == js.Object) {
return value.js_obj;
}
const persistent_object = self.identity_map.get(@intFromPtr(value)) orelse {
return error.InvalidThisForCallback;
};
return persistent_object.castToObject();
}
// == Executors == // == Executors ==
pub fn eval(self: *Context, src: []const u8, name: ?[]const u8) !void { pub fn eval(self: *Context, src: []const u8, name: ?[]const u8) !void {
_ = try self.exec(src, name); _ = try self.exec(src, name);
@@ -431,7 +452,7 @@ pub fn zigValueToJs(self: *Context, value: anytype, comptime opts: Caller.CallOp
var js_arr = v8.Array.init(isolate, value.len); var js_arr = v8.Array.init(isolate, value.len);
var js_obj = js_arr.castTo(v8.Object); var js_obj = js_arr.castTo(v8.Object);
for (value, 0..) |v, i| { for (value, 0..) |v, i| {
const js_val = try self.zigValueToJs(v, opts); const js_val = try self.zigValueToJs(v, .{});
if (js_obj.setValueAtIndex(v8_context, @intCast(i), js_val) == false) { if (js_obj.setValueAtIndex(v8_context, @intCast(i), js_val) == false) {
return error.FailedToCreateArray; return error.FailedToCreateArray;
} }
@@ -556,7 +577,7 @@ pub fn zigValueToJs(self: *Context, value: anytype, comptime opts: Caller.CallOp
}, },
.optional => { .optional => {
if (value) |v| { if (value) |v| {
return self.zigValueToJs(v, opts); return self.zigValueToJs(v, .{});
} }
// would be handled by simpleZigValueToJs // would be handled by simpleZigValueToJs
unreachable; unreachable;

View File

@@ -74,23 +74,26 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
const env = self.env; const env = self.env;
const isolate = env.isolate; const isolate = env.isolate;
const arena = self.context_arena.allocator();
var v8_context: v8.Context = blk: { var v8_context: v8.Context = blk: {
var temp_scope: v8.HandleScope = undefined; var temp_scope: v8.HandleScope = undefined;
v8.HandleScope.init(&temp_scope, isolate); v8.HandleScope.init(&temp_scope, isolate);
defer temp_scope.deinit(); defer temp_scope.deinit();
// Creates a global template that inherits from Window. if (comptime IS_DEBUG) {
const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate, env.templates); // Getting this into the snapshot is tricky (anything involving the
// global is tricky). Easier to do here, and in debug mode, we're
// fine with paying the small perf hit.
const js_global = v8.FunctionTemplate.initDefault(isolate);
const global_template = js_global.getInstanceTemplate();
// Add the named property handler global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{
global_template.setNamedProperty(v8.NamedPropertyHandlerConfiguration{ .getter = unknownPropertyCallback,
.getter = unknownPropertyCallback, .flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings, }, null);
}, null); }
const context_local = v8.Context.init(isolate, global_template, null); const context_local = v8.Context.init(isolate, null, null);
const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext(); const v8_context = v8.Persistent(v8.Context).init(isolate, context_local).castToContext();
break :blk v8_context; break :blk v8_context;
}; };
@@ -121,7 +124,7 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
.handle_scope = handle_scope, .handle_scope = handle_scope,
.script_manager = &page._script_manager, .script_manager = &page._script_manager,
.call_arena = page.call_arena, .call_arena = page.call_arena,
.arena = arena, .arena = self.context_arena.allocator(),
}; };
var context = &self.context.?; var context = &self.context.?;
@@ -156,9 +159,9 @@ pub fn resumeExecution(self: *const ExecutionWorld) void {
pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 { pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info); const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
const context = Context.fromIsolate(info.getIsolate()); const context = Context.fromIsolate(info.getIsolate());
const maybe_property: ?[]u8 = context.valueToString(.{ .handle = c_name.? }, .{}) catch null;
const property = context.valueToString(.{ .handle = c_name.? }, .{}) catch "???";
const ignored = std.StaticStringMap(void).initComptime(.{ const ignored = std.StaticStringMap(void).initComptime(.{
.{ "process", {} }, .{ "process", {} },
@@ -182,26 +185,12 @@ pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C
.{ "CLOSURE_FLAGS", {} }, .{ "CLOSURE_FLAGS", {} },
}); });
if (maybe_property) |prop| { if (!ignored.has(property)) {
if (!ignored.has(prop)) { log.debug(.unknown_prop, "unkown global property", .{
const page = context.page; .info = "but the property can exist in pure JS",
const document = page.document; .stack = context.stackTrace() catch "???",
.property = property,
if (document.getElementById(prop, page)) |el| { });
const js_value = context.zigValueToJs(el, .{}) catch {
return v8.Intercepted.No;
};
info.getReturnValue().set(js_value);
return v8.Intercepted.Yes;
}
log.debug(.unknown_prop, "unknown global property", .{
.info = "but the property can exist in pure JS",
.stack = context.stackTrace() catch "???",
.property = prop,
});
}
} }
return v8.Intercepted.No; return v8.Intercepted.No;

View File

@@ -116,29 +116,7 @@ pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, a
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T { pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
const context = self.context; const context = self.context;
// When we're calling a function from within JavaScript itself, this isn't const js_this = try context.valueToExistingObject(this);
// necessary. We're within a Caller instantiation, which will already have
// incremented the call_depth and it won't decrement it until the Caller is
// done.
// But some JS functions are initiated from Zig code, and not v8. For
// example, Observers, some event and window callbacks. In those cases, we
// need to increase the call_depth so that the call_arena remains valid for
// the duration of the function call. If we don't do this, the call_arena
// will be reset after each statement of the function which executes Zig code.
const call_depth = context.call_depth;
context.call_depth = call_depth + 1;
defer context.call_depth = call_depth;
const js_this = blk: {
if (@TypeOf(this) == v8.Object) {
break :blk this;
}
if (@TypeOf(this) == js.Object) {
break :blk this.js_obj;
}
break :blk try context.zigValueToJs(this, .{});
};
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args; const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;

View File

@@ -54,7 +54,7 @@ pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{
const context = self.context; const context = self.context;
const js_key = v8.String.initUtf8(context.isolate, key); const js_key = v8.String.initUtf8(context.isolate, key);
const js_value = try context.zigValueToJs(value, .{}); const js_value = try context.zigValueToJs(value);
const res = self.js_obj.defineOwnProperty(context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false; const res = self.js_obj.defineOwnProperty(context.v8_context, js_key.toName(), js_value, @bitCast(opts)) orelse false;
if (!res) { if (!res) {

View File

@@ -113,17 +113,6 @@ fn isValid(self: Snapshot) bool {
return v8.SnapshotCreator.startupDataIsValid(self.startup_data); return v8.SnapshotCreator.startupDataIsValid(self.startup_data);
} }
pub fn createGlobalTemplate(isolate: v8.Isolate, templates: []const v8.FunctionTemplate) v8.ObjectTemplate {
// Set up the global template to inherit from Window's template
// This way the global object gets all Window properties through inheritance
const js_global = v8.FunctionTemplate.initDefault(isolate);
js_global.setClassName(v8.String.initUtf8(isolate, "Window"));
// Find Window in JsApis by name (avoids circular import)
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
js_global.inherit(templates[window_index]);
return js_global.getInstanceTemplate();
}
pub fn create(allocator: Allocator) !Snapshot { pub fn create(allocator: Allocator) !Snapshot {
var external_references = collectExternalReferences(); var external_references = collectExternalReferences();
@@ -165,7 +154,14 @@ pub fn create(allocator: Allocator) !Snapshot {
// Set up the global template to inherit from Window's template // Set up the global template to inherit from Window's template
// This way the global object gets all Window properties through inheritance // This way the global object gets all Window properties through inheritance
const global_template = createGlobalTemplate(isolate, templates[0..]); const js_global = v8.FunctionTemplate.initDefault(isolate);
js_global.setClassName(v8.String.initUtf8(isolate, "Window"));
// Find Window in JsApis by name (avoids circular import)
const window_index = comptime bridge.JsApiLookup.getId(Window.JsApi);
js_global.inherit(templates[window_index]);
const global_template = js_global.getInstanceTemplate();
const context = v8.Context.init(isolate, global_template, null); const context = v8.Context.init(isolate, global_template, null);
context.enter(); context.enter();
@@ -411,7 +407,7 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
}, },
bridge.Function => { bridge.Function => {
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func); const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
const js_name = v8.String.initUtf8(isolate, name).toName(); const js_name: v8.Name = v8.String.initUtf8(isolate, name).toName();
if (value.static) { if (value.static) {
template.set(js_name, function_template, v8.PropertyAttribute.None); template.set(js_name, function_template, v8.PropertyAttribute.None);
} else { } else {
@@ -460,12 +456,6 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
instance_template.markAsUndetectable(); instance_template.markAsUndetectable();
instance_template.setCallAsFunctionHandler(JsApi.Meta.callable.func); instance_template.setCallAsFunctionHandler(JsApi.Meta.callable.func);
} }
if (@hasDecl(JsApi.Meta, "name")) {
const js_name = v8.Symbol.getToStringTag(isolate).toName();
const instance_template = template.getInstanceTemplate();
instance_template.set(js_name, v8.String.initUtf8(isolate, JsApi.Meta.name), v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
}
} }
fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt { fn protoIndexLookup(comptime JsApi: type) ?bridge.JsApiLookup.BackingInt {

View File

@@ -41,14 +41,6 @@ pub fn isArray(self: Value) bool {
return self.js_val.isArray(); return self.js_val.isArray();
} }
pub fn isNull(self: Value) bool {
return self.js_val.isNull();
}
pub fn isUndefined(self: Value) bool {
return self.js_val.isUndefined();
}
pub fn toString(self: Value, allocator: Allocator) ![]const u8 { pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.js_val, .{ .allocator = allocator }); return self.context.valueToString(self.js_val, .{ .allocator = allocator });
} }
@@ -69,10 +61,6 @@ pub fn persist(self: Value) !Value {
return Value{ .context = context, .js_val = persisted.toValue() }; return Value{ .context = context, .js_val = persisted.toValue() };
} }
pub fn toZig(self: Value, comptime T: type) !T {
return self.context.jsValueToZig(T, self.js_val);
}
pub fn toObject(self: Value) js.Object { pub fn toObject(self: Value) js.Object {
return .{ return .{
.context = self.context, .context = self.context,

View File

@@ -98,29 +98,6 @@ pub fn parse(self: *Parser, html: []const u8) void {
); );
} }
pub fn parseXML(self: *Parser, xml: []const u8) void {
h5e.xml5ever_parse_document(
xml.ptr,
xml.len,
&self.container,
self,
createElementCallback,
getDataCallback,
appendCallback,
parseErrorCallback,
popCallback,
createCommentCallback,
createProcessingInstruction,
appendDoctypeToDocument,
addAttrsIfMissingCallback,
getTemplateContentsCallback,
removeFromParentCallback,
reparentChildrenCallback,
appendBeforeSiblingCallback,
appendBasedOnParentNodeCallback,
);
}
pub fn parseFragment(self: *Parser, html: []const u8) void { pub fn parseFragment(self: *Parser, html: []const u8) void {
h5e.html5ever_parse_fragment( h5e.html5ever_parse_fragment(
html.ptr, html.ptr,

View File

@@ -171,24 +171,3 @@ pub const NodeOrText = extern struct {
text: []const u8, text: []const u8,
}; };
}; };
pub extern "c" fn xml5ever_parse_document(
html: [*c]const u8,
len: usize,
doc: *anyopaque,
ctx: *anyopaque,
createElementCallback: *const fn (ctx: *anyopaque, data: *anyopaque, QualName, AttributeIterator) callconv(.c) ?*anyopaque,
elemNameCallback: *const fn (node_ref: *anyopaque) callconv(.c) *anyopaque,
appendCallback: *const fn (ctx: *anyopaque, parent_ref: *anyopaque, NodeOrText) callconv(.c) void,
parseErrorCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) void,
popCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque) callconv(.c) void,
createCommentCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) ?*anyopaque,
createProcessingInstruction: *const fn (ctx: *anyopaque, StringSlice, StringSlice) callconv(.c) ?*anyopaque,
appendDoctypeToDocument: *const fn (ctx: *anyopaque, StringSlice, StringSlice, StringSlice) callconv(.c) void,
addAttrsIfMissingCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque, AttributeIterator) callconv(.c) void,
getTemplateContentsCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) ?*anyopaque,
removeFromParentCallback: *const fn (ctx: *anyopaque, target_ref: *anyopaque) callconv(.c) void,
reparentChildrenCallback: *const fn (ctx: *anyopaque, node_ref: *anyopaque, new_parent_ref: *anyopaque) callconv(.c) void,
appendBeforeSiblingCallback: *const fn (ctx: *anyopaque, sibling_ref: *anyopaque, NodeOrText) callconv(.c) void,
appendBasedOnParentNodeCallback: *const fn (ctx: *anyopaque, element_ref: *anyopaque, prev_element_ref: *anyopaque, NodeOrText) callconv(.c) void,
) void;

View File

@@ -107,6 +107,19 @@
} }
</script> </script>
<script id=unsupportedMimeType>
{
const parser = new DOMParser();
// Should throw an error for unsupported MIME types
testing.withError((err) => {
testing.expectEqual('NotSupported', err.message);
}, () => {
parser.parseFromString('<div>test</div>', 'application/xml');
});
}
</script>
<script id=getElementById> <script id=getElementById>
{ {
const doc = new DOMParser().parseFromString('<div id="new-node">new-node</div>', 'text/html'); const doc = new DOMParser().parseFromString('<div id="new-node">new-node</div>', 'text/html');
@@ -231,161 +244,3 @@
testing.expectEqual('<html><head></head><body>spice</body></html>', new DOMParser().parseFromString('spice', "text/html").documentElement.outerHTML); testing.expectEqual('<html><head></head><body>spice</body></html>', new DOMParser().parseFromString('spice', "text/html").documentElement.outerHTML);
testing.expectEqual('<html><head></head><body></body></html>', new DOMParser().parseFromString('<html></html>', "text/html").documentElement.outerHTML); testing.expectEqual('<html><head></head><body></body></html>', new DOMParser().parseFromString('<html></html>', "text/html").documentElement.outerHTML);
</script> </script>
<script id=parse-xml>
{
const sampleXML = `<?xml version="1.0"?>
<catalog>
<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developer's Guide</title>
<genre>Computer</genre>
<price>44.95</price>
<publish_date>2000-10-01</publish_date>
<description>An in-depth look at creating applications
with XML.</description>
</book>
<book id="bk102">
<author>Ralls, Kim</author>
<title>Midnight Rain</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2000-12-16</publish_date>
<description>A former architect battles corporate zombies,
an evil sorceress, and her own childhood to become queen
of the world.</description>
</book>
<book id="bk103">
<author>Corets, Eva</author>
<title>Maeve Ascendant</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2000-11-17</publish_date>
<description>After the collapse of a nanotechnology
society in England, the young survivors lay the
foundation for a new society.</description>
</book>
<book id="bk104">
<author>Corets, Eva</author>
<title>Oberon's Legacy</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2001-03-10</publish_date>
<description>In post-apocalypse England, the mysterious
agent known only as Oberon helps to create a new life
for the inhabitants of London. Sequel to Maeve
Ascendant.</description>
</book>
<book id="bk105">
<author>Corets, Eva</author>
<title>The Sundered Grail</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2001-09-10</publish_date>
<description>The two daughters of Maeve, half-sisters,
battle one another for control of England. Sequel to
Oberon's Legacy.</description>
</book>
<book id="bk106">
<author>Randall, Cynthia</author>
<title>Lover Birds</title>
<genre>Romance</genre>
<price>4.95</price>
<publish_date>2000-09-02</publish_date>
<description>When Carla meets Paul at an ornithology
conference, tempers fly as feathers get ruffled.</description>
</book>
<book id="bk107">
<author>Thurman, Paula</author>
<title>Splish Splash</title>
<genre>Romance</genre>
<price>4.95</price>
<publish_date>2000-11-02</publish_date>
<description>A deep sea diver finds true love twenty
thousand leagues beneath the sea.</description>
</book>
<book id="bk108">
<author>Knorr, Stefan</author>
<title>Creepy Crawlies</title>
<genre>Horror</genre>
<price>4.95</price>
<publish_date>2000-12-06</publish_date>
<description>An anthology of horror stories about roaches,
centipedes, scorpions and other insects.</description>
</book>
<book id="bk109">
<author>Kress, Peter</author>
<title>Paradox Lost</title>
<genre>Science Fiction</genre>
<price>6.95</price>
<publish_date>2000-11-02</publish_date>
<description>After an inadvertant trip through a Heisenberg
Uncertainty Device, James Salway discovers the problems
of being quantum.</description>
</book>
<book id="bk110">
<author>O'Brien, Tim</author>
<title>Microsoft .NET: The Programming Bible</title>
<genre>Computer</genre>
<price>36.95</price>
<publish_date>2000-12-09</publish_date>
<description>Microsoft's .NET initiative is explored in
detail in this deep programmer's reference.</description>
</book>
<book id="bk111">
<author>O'Brien, Tim</author>
<title>MSXML3: A Comprehensive Guide</title>
<genre>Computer</genre>
<price>36.95</price>
<publish_date>2000-12-01</publish_date>
<description>The Microsoft MSXML3 parser is covered in
detail, with attention to XML DOM interfaces, XSLT processing,
SAX and more.</description>
</book>
<book id="bk112">
<author>Galos, Mike</author>
<title>Visual Studio 7: A Comprehensive Guide</title>
<genre>Computer</genre>
<price>49.95</price>
<publish_date>2001-04-16</publish_date>
<description>Microsoft Visual Studio 7 is explored in depth,
looking at how Visual Basic, Visual C++, C#, and ASP+ are
integrated into a comprehensive development
environment.</description>
</book>
</catalog>`;
const parser = new DOMParser();
const mimes = [
"text/xml",
"application/xml",
"application/xhtml+xml",
"image/svg+xml",
];
for (const mime of mimes) {
const doc = parser.parseFromString(sampleXML, "text/xml");
const { firstChild: { childNodes, children: collection, tagName }, children } = doc;
// doc.
testing.expectEqual(true, doc instanceof XMLDocument);
testing.expectEqual(1, children.length);
// firstChild.
// TODO: Modern browsers expect this in lowercase.
testing.expectEqual("CATALOG", tagName);
testing.expectEqual(25, childNodes.length);
testing.expectEqual(12, collection.length);
// Check children of first child.
for (let i = 0; i < collection.length; i++) {
const {children: elements, id} = collection.item(i);
testing.expectEqual("bk" + (100 + i + 1), id);
// TODO: Modern browsers expect these in lowercase.
testing.expectEqual("AUTHOR", elements.item(0).tagName);
testing.expectEqual("TITLE", elements.item(1).tagName);
testing.expectEqual("GENRE", elements.item(2).tagName);
testing.expectEqual("PRICE", elements.item(3).tagName);
testing.expectEqual("PUBLISH_DATE", elements.item(4).tagName);
testing.expectEqual("DESCRIPTION", elements.item(5).tagName);
}
}
}
</script>

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id="test">first</div>
<div id="test">second</div>
<script id=duplicateIds>
const first = document.getElementById('test');
testing.expectEqual('first', first.textContent);
first.remove();
const second = document.getElementById('test');
testing.expectEqual('second', second.textContent);
// second.remove();
// testing.expectEqual(null, document.getElementById('test'));
</script>

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id=d1>hello <em>world</em></div>
<script id=outerHTML>
const d1 = $('#d1');
testing.expectEqual('<div id=\"d1\">hello <em>world</em></div>', d1.outerHTML);
d1.outerHTML = '<p id=p1>spice</p>';
// setting outerHTML doesn't update what d1 points to
testing.expectEqual('<div id="d1">hello <em>world</em></div>', d1.outerHTML);
// but it does update the document
testing.expectEqual(null, document.getElementById('d1'));
testing.expectEqual(true, document.getElementById('p1') != null);
testing.expectEqual('<p id="p1">spice</p>', document.getElementById('p1').outerHTML);
// testing.expectEqual(true, document.body.outerHTML.replaceAll(/\n/g, '').startsWith('<body><p id="p1">spice</p><script id="outerHTML">'));
// document.getElementById('p1').outerHTML = '';
// testing.expectEqual(null, document.getElementById('p1'));
// testing.expectEqual(true, document.body.outerHTML.replaceAll(/\n/g, '').startsWith('<body><script id="outerHTML">'));
</script>

View File

@@ -1,53 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id="removeListenerDuringDispatch">
const target = document.createElement("div");
let listener1Called = 0;
let listener2Called = 0;
let listener3Called = 0;
function listener1() {
listener1Called++;
console.warn("listener1 called, removing listener2 and adding listener3");
target.removeEventListener("foo", listener2);
target.addEventListener("foo", listener3);
}
function listener2() {
listener2Called++;
console.warn("listener2 called (SHOULD NOT HAPPEN)");
}
function listener3() {
listener3Called++;
console.warn("listener3 called (SHOULD NOT HAPPEN IN FIRST DISPATCH)");
}
target.addEventListener("foo", listener1);
target.addEventListener("foo", listener2);
console.warn("Dispatching first event");
target.dispatchEvent(new Event("foo"));
console.warn("After first dispatch:");
console.warn(" listener1Called:", listener1Called);
console.warn(" listener2Called:", listener2Called);
console.warn(" listener3Called:", listener3Called);
testing.expectEqual(1, listener1Called);
testing.expectEqual(0, listener2Called);
testing.expectEqual(0, listener3Called);
console.warn("Dispatching second event");
target.dispatchEvent(new Event("foo"));
console.warn("After second dispatch:");
console.warn(" listener1Called:", listener1Called);
console.warn(" listener2Called:", listener2Called);
console.warn(" listener3Called:", listener3Called);
testing.expectEqual(2, listener1Called);
testing.expectEqual(0, listener2Called);
testing.expectEqual(1, listener3Called);
</script>

View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body></body>
<script id="adoptNode">
const old = document.implementation.createHTMLDocument("");
const div = old.createElement("div");
div.appendChild(old.createTextNode("text"));
testing.expectEqual(old, div.ownerDocument);
testing.expectEqual(old, div.firstChild.ownerDocument);
document.body.appendChild(div);
testing.expectEqual(document, div.ownerDocument);
testing.expectEqual(document, div.firstChild.ownerDocument);
</script>

View File

@@ -37,7 +37,4 @@
testing.expectEqual(null, c2.parentNode); testing.expectEqual(null, c2.parentNode);
assertChildren([c3, c4], d1) assertChildren([c3, c4], d1)
assertChildren([], d2) assertChildren([], d2)
testing.expectEqual(c3, d1.replaceChild(c3, c3));
assertChildren([c3, c4], d1)
</script> </script>

View File

@@ -376,447 +376,3 @@
testing.expectEqual('Bold', fragment.childNodes[0].textContent); testing.expectEqual('Bold', fragment.childNodes[0].textContent);
} }
</script> </script>
<script id=offset_validation_setStart>
{
const range = document.createRange();
const p1 = $('#p1');
// Test setStart with offset beyond node length
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.setStart(p1, 999);
});
// Test with negative offset (wraps to large u32)
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.setStart(p1.firstChild, -1);
});
}
</script>
<script id=offset_validation_setEnd>
{
const range = document.createRange();
const p1 = $('#p1');
// Test setEnd with offset beyond node length
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.setEnd(p1, 999);
});
// Test with text node
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.setEnd(p1.firstChild, 9999);
});
}
</script>
<script id=comparePoint_basic>
{
// Create fresh elements to avoid DOM pollution from other tests
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = 'First paragraph text';
p2.textContent = 'Second paragraph text';
div.appendChild(p1);
div.appendChild(p2);
const range = document.createRange();
range.setStart(p1.firstChild, 0);
range.setEnd(p2.firstChild, 5);
// Point before range
testing.expectEqual(-1, range.comparePoint(div, 0));
// Point at start boundary
testing.expectEqual(0, range.comparePoint(p1.firstChild, 0));
// Point inside range (in p1)
testing.expectEqual(0, range.comparePoint(p1.firstChild, 3));
// Point inside range (in p2)
testing.expectEqual(0, range.comparePoint(p2.firstChild, 2));
// Point at end boundary
testing.expectEqual(0, range.comparePoint(p2.firstChild, 5));
// Point after range
testing.expectEqual(1, range.comparePoint(p2.firstChild, 10));
}
</script>
<script id=comparePoint_validation>
{
// Create fresh element
const p1 = document.createElement('p');
p1.textContent = 'Test content';
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// Test comparePoint with invalid offset
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.comparePoint(p1, 20);
});
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.comparePoint(p1.firstChild, -1);
});
}
</script>
<script id=different_document_collapse>
{
// Create fresh element in current document
const p1 = document.createElement('p');
p1.textContent = 'Local content';
const range = document.createRange();
// Create a foreign document
const foreignDoc = document.implementation.createHTMLDocument('');
const foreignP = foreignDoc.createElement('p');
foreignP.textContent = 'Foreign';
foreignDoc.body.appendChild(foreignP);
// Set up range in current document
range.setStart(p1, 0);
range.setEnd(p1, 1);
testing.expectEqual(false, range.collapsed);
// Setting start to foreign document should collapse to that point
range.setStart(foreignP, 0);
testing.expectEqual(true, range.collapsed);
testing.expectEqual(foreignP, range.startContainer);
testing.expectEqual(foreignP, range.endContainer);
}
</script>
<script id=detached_node_collapse>
{
// Create fresh element
const p1 = document.createElement('p');
p1.textContent = 'Attached content';
const range = document.createRange();
// Create a detached element
const detached = document.createElement('div');
detached.textContent = 'Detached';
// Set up range in document
range.setStart(p1, 0);
range.setEnd(p1, 1);
testing.expectEqual(false, range.collapsed);
// Setting end to detached node should collapse
range.setEnd(detached.firstChild, 0);
testing.expectEqual(true, range.collapsed);
testing.expectEqual(detached.firstChild, range.startContainer);
testing.expectEqual(detached.firstChild, range.endContainer);
}
</script>
<script id=isPointInRange_basic>
{
// Create fresh elements
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = 'First paragraph';
p2.textContent = 'Second paragraph';
div.appendChild(p1);
div.appendChild(p2);
const range = document.createRange();
range.setStart(p1.firstChild, 5);
range.setEnd(p2.firstChild, 6);
// Point before range
testing.expectEqual(false, range.isPointInRange(p1.firstChild, 0));
// Point at start boundary
testing.expectEqual(true, range.isPointInRange(p1.firstChild, 5));
// Point inside range
testing.expectEqual(true, range.isPointInRange(p1.firstChild, 7));
testing.expectEqual(true, range.isPointInRange(p2.firstChild, 3));
// Point at end boundary
testing.expectEqual(true, range.isPointInRange(p2.firstChild, 6));
// Point after range
testing.expectEqual(false, range.isPointInRange(p2.firstChild, 10));
}
</script>
<script id=isPointInRange_different_root>
{
// Create element in current document
const p1 = document.createElement('p');
p1.textContent = 'Local content';
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// Create element in different document
const foreignDoc = document.implementation.createHTMLDocument('');
const foreignP = foreignDoc.createElement('p');
foreignP.textContent = 'Foreign';
// Point in different root should return false (not throw)
testing.expectEqual(false, range.isPointInRange(foreignP, 0));
}
</script>
<script id=isPointInRange_validation>
{
const p1 = document.createElement('p');
p1.textContent = 'Test content';
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// Invalid offset should throw IndexSizeError
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.isPointInRange(p1, 999);
});
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
range.isPointInRange(p1.firstChild, 9999);
});
}
</script>
<script id=intersectsNode_basic>
{
// Create fresh elements
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
const p3 = document.createElement('p');
p1.textContent = 'First';
p2.textContent = 'Second';
p3.textContent = 'Third';
div.appendChild(p1);
div.appendChild(p2);
div.appendChild(p3);
const range = document.createRange();
range.setStart(p1.firstChild, 2);
range.setEnd(p2.firstChild, 3);
// Node that intersects (p1 contains the start)
testing.expectEqual(true, range.intersectsNode(p1));
// Node that intersects (p2 contains the end)
testing.expectEqual(true, range.intersectsNode(p2));
// Node that doesn't intersect (p3 is after the range)
testing.expectEqual(false, range.intersectsNode(p3));
// Container intersects
testing.expectEqual(true, range.intersectsNode(div));
}
</script>
<script id=intersectsNode_detached>
{
const div = document.createElement('div');
const p1 = document.createElement('p');
p1.textContent = 'Content';
div.appendChild(p1);
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// The root node (div) should return true when it has no parent
// (Note: div is detached, so it's in the same tree as the range)
testing.expectEqual(true, range.intersectsNode(div));
}
</script>
<script id=intersectsNode_different_root>
{
const p1 = document.createElement('p');
p1.textContent = 'Local';
const range = document.createRange();
range.setStart(p1, 0);
range.setEnd(p1, 1);
// Node in different document should return false
const foreignDoc = document.implementation.createHTMLDocument('');
const foreignP = foreignDoc.createElement('p');
testing.expectEqual(false, range.intersectsNode(foreignP));
}
</script>
<script id=commonAncestorContainer_same_node>
{
const p = document.createElement('p');
p.textContent = 'Content';
const range = document.createRange();
range.setStart(p.firstChild, 0);
range.setEnd(p.firstChild, 3);
// Both boundaries in same text node, so that's the common ancestor
testing.expectEqual(p.firstChild, range.commonAncestorContainer);
}
</script>
<script id=commonAncestorContainer_siblings>
{
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = 'First';
p2.textContent = 'Second';
div.appendChild(p1);
div.appendChild(p2);
const range = document.createRange();
range.setStart(p1.firstChild, 0);
range.setEnd(p2.firstChild, 3);
// Start and end in different siblings, common ancestor is the parent div
testing.expectEqual(div, range.commonAncestorContainer);
}
</script>
<script id=commonAncestorContainer_nested>
{
const div = document.createElement('div');
const section = document.createElement('section');
const p = document.createElement('p');
const span = document.createElement('span');
p.textContent = 'Text';
span.textContent = 'Span';
div.appendChild(section);
section.appendChild(p);
div.appendChild(span);
const range = document.createRange();
range.setStart(p.firstChild, 0);
range.setEnd(span.firstChild, 2);
// Common ancestor of deeply nested p and sibling span is div
testing.expectEqual(div, range.commonAncestorContainer);
}
</script>
<script id=compareBoundaryPoints_constants>
{
// Test that the constants are defined
testing.expectEqual(0, Range.START_TO_START);
testing.expectEqual(1, Range.START_TO_END);
testing.expectEqual(2, Range.END_TO_END);
testing.expectEqual(3, Range.END_TO_START);
}
</script>
<script id=compareBoundaryPoints_basic>
{
const div = document.createElement('div');
const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = 'First paragraph';
p2.textContent = 'Second paragraph';
div.appendChild(p1);
div.appendChild(p2);
const range1 = document.createRange();
range1.setStart(p1.firstChild, 0);
range1.setEnd(p1.firstChild, 5);
const range2 = document.createRange();
range2.setStart(p1.firstChild, 3);
range2.setEnd(p2.firstChild, 5);
// range1 start is before range2 start
testing.expectEqual(-1, range1.compareBoundaryPoints(Range.START_TO_START, range2));
// range1 start is before range2 end
testing.expectEqual(-1, range1.compareBoundaryPoints(Range.START_TO_END, range2));
// range1 end is after range2 start
testing.expectEqual(1, range1.compareBoundaryPoints(Range.END_TO_START, range2));
// range1 end is before range2 end
testing.expectEqual(-1, range1.compareBoundaryPoints(Range.END_TO_END, range2));
}
</script>
<script id=compareBoundaryPoints_same_range>
{
const p = document.createElement('p');
p.textContent = 'Content';
const range = document.createRange();
range.setStart(p.firstChild, 2);
range.setEnd(p.firstChild, 5);
// Comparing a range to itself should return 0
testing.expectEqual(0, range.compareBoundaryPoints(Range.START_TO_START, range));
testing.expectEqual(0, range.compareBoundaryPoints(Range.END_TO_END, range));
// Start is before end
testing.expectEqual(-1, range.compareBoundaryPoints(Range.START_TO_END, range));
// End is after start
testing.expectEqual(1, range.compareBoundaryPoints(Range.END_TO_START, range));
}
</script>
<script id=compareBoundaryPoints_invalid_how>
{
const p = document.createElement('p');
p.textContent = 'Test';
const range1 = document.createRange();
const range2 = document.createRange();
range1.setStart(p, 0);
range2.setStart(p, 0);
// Invalid how parameter should throw NotSupportedError
testing.expectError('NotSupportedError: Not Supported', () => {
range1.compareBoundaryPoints(4, range2);
});
testing.expectError('NotSupportedError: Not Supported', () => {
range1.compareBoundaryPoints(99, range2);
});
}
</script>
<script id=compareBoundaryPoints_different_root>
{
const p1 = document.createElement('p');
p1.textContent = 'Local';
const range1 = document.createRange();
range1.setStart(p1, 0);
range1.setEnd(p1, 1);
// Create range in different document
const foreignDoc = document.implementation.createHTMLDocument('');
const foreignP = foreignDoc.createElement('p');
foreignP.textContent = 'Foreign';
const range2 = foreignDoc.createRange();
range2.setStart(foreignP, 0);
range2.setEnd(foreignP, 1);
// Comparing ranges in different documents should throw WrongDocumentError
testing.expectError('WrongDocumentError: wrong_document_error', () => {
range1.compareBoundaryPoints(Range.START_TO_START, range2);
});
}
</script>

View File

@@ -1,26 +0,0 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<div id=i></div>
<div id=testDiv></div>
<span id=mySpan></span>
<p id=paragraph></p>
<script id=named_access_global>
testing.expectEqual('i', i.id);
testing.expectEqual('testDiv',testDiv.id);
testing.expectEqual('mySpan', mySpan.id);
testing.expectEqual('paragraph', paragraph.id);
</script>
<script id=named_access_window>
testing.expectEqual('i', window.i.id);
testing.expectEqual('testDiv', window.testDiv.id);
testing.expectEqual('mySpan', window.mySpan.id);
testing.expectEqual('paragraph', window.paragraph.id);
</script>
<script id=named_access_shadowing>
const i = 100;
testing.expectEqual(100, i);
</script>

View File

@@ -69,19 +69,6 @@ pub fn getCollapsed(self: *const AbstractRange) bool {
self._start_offset == self._end_offset; self._start_offset == self._end_offset;
} }
pub fn getCommonAncestorContainer(self: *const AbstractRange) *Node {
// Let container be start container
var container = self._start_container;
// While container is not an inclusive ancestor of end container
while (!isInclusiveAncestorOf(container, self._end_container)) {
// Let container be container's parent
container = container.parentNode() orelse break;
}
return container;
}
pub fn isStartAfterEnd(self: *const AbstractRange) bool { pub fn isStartAfterEnd(self: *const AbstractRange) bool {
return compareBoundaryPoints( return compareBoundaryPoints(
self._start_container, self._start_container,
@@ -97,7 +84,7 @@ const BoundaryComparison = enum {
after, after,
}; };
pub fn compareBoundaryPoints( fn compareBoundaryPoints(
node_a: *Node, node_a: *Node,
offset_a: u32, offset_a: u32,
node_b: *Node, node_b: *Node,
@@ -208,13 +195,6 @@ fn isAncestorOf(potential_ancestor: *Node, node: *Node) bool {
return false; return false;
} }
fn isInclusiveAncestorOf(potential_ancestor: *Node, node: *Node) bool {
if (potential_ancestor == node) {
return true;
}
return isAncestorOf(potential_ancestor, node);
}
pub const JsApi = struct { pub const JsApi = struct {
pub const bridge = js.Bridge(AbstractRange); pub const bridge = js.Bridge(AbstractRange);
@@ -229,5 +209,4 @@ pub const JsApi = struct {
pub const endContainer = bridge.accessor(AbstractRange.getEndContainer, null, .{}); pub const endContainer = bridge.accessor(AbstractRange.getEndContainer, null, .{});
pub const endOffset = bridge.accessor(AbstractRange.getEndOffset, null, .{}); pub const endOffset = bridge.accessor(AbstractRange.getEndOffset, null, .{});
pub const collapsed = bridge.accessor(AbstractRange.getCollapsed, null, .{}); pub const collapsed = bridge.accessor(AbstractRange.getCollapsed, null, .{});
pub const commonAncestorContainer = bridge.accessor(AbstractRange.getCommonAncestorContainer, null, .{});
}; };

View File

@@ -147,7 +147,7 @@ pub fn format(self: *const CData, writer: *std.io.Writer) !void {
} }
pub fn getLength(self: *const CData) usize { pub fn getLength(self: *const CData) usize {
return std.unicode.utf8CountCodepoints(self._data) catch self._data.len; return self._data.len;
} }
pub fn isEqualNode(self: *const CData, other: *const CData) bool { pub fn isEqualNode(self: *const CData, other: *const CData) bool {

View File

@@ -120,6 +120,11 @@ pub const JsApi = struct {
pub const createDocument = bridge.function(DOMImplementation.createDocument, .{}); pub const createDocument = bridge.function(DOMImplementation.createDocument, .{});
pub const createHTMLDocument = bridge.function(DOMImplementation.createHTMLDocument, .{}); pub const createHTMLDocument = bridge.function(DOMImplementation.createHTMLDocument, .{});
pub const hasFeature = bridge.function(DOMImplementation.hasFeature, .{}); pub const hasFeature = bridge.function(DOMImplementation.hasFeature, .{});
pub const toString = bridge.function(_toString, .{});
fn _toString(_: *const DOMImplementation) []const u8 {
return "[object DOMImplementation]";
}
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

@@ -19,13 +19,8 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Parser = @import("../parser/Parser.zig");
const HTMLDocument = @import("HTMLDocument.zig"); const HTMLDocument = @import("HTMLDocument.zig");
const XMLDocument = @import("XMLDocument.zig");
const ProcessingInstruction = @import("../webapi/cdata/ProcessingInstruction.zig");
const DOMParser = @This(); const DOMParser = @This();
@@ -33,78 +28,34 @@ pub fn init() DOMParser {
return .{}; return .{};
} }
pub const HTMLDocumentOrXMLDocument = union(enum) { pub fn parseFromString(self: *const DOMParser, html: []const u8, mime_type: []const u8, page: *Page) !*HTMLDocument {
html_document: *HTMLDocument, _ = self;
xml_document: *XMLDocument,
};
pub fn parseFromString( // For now, only support text/html
_: *const DOMParser, if (!std.mem.eql(u8, mime_type, "text/html")) {
html: []const u8, return error.NotSupported;
mime_type: []const u8, }
page: *Page,
) !HTMLDocumentOrXMLDocument {
const maybe_target_mime = std.meta.stringToEnum(enum {
@"text/html",
@"text/xml",
@"application/xml",
@"application/xhtml+xml",
@"image/svg+xml",
}, mime_type);
if (maybe_target_mime) |target_mime| switch (target_mime) { // Create a new HTMLDocument
.@"text/html" => { const doc = try page._factory.document(HTMLDocument{
// Create a new HTMLDocument ._proto = undefined,
const doc = try page._factory.document(HTMLDocument{ });
._proto = undefined,
});
var normalized = std.mem.trim(u8, html, &std.ascii.whitespace); var normalized = std.mem.trim(u8, html, &std.ascii.whitespace);
if (normalized.len == 0) { if (normalized.len == 0) {
normalized = "<html></html>"; normalized = "<html></html>";
} }
// Parse HTML into the document // Parse HTML into the document
var parser = Parser.init(page.arena, doc.asNode(), page); const Parser = @import("../parser/Parser.zig");
parser.parse(normalized); var parser = Parser.init(page.arena, doc.asNode(), page);
parser.parse(normalized);
if (parser.err) |pe| { if (parser.err) |pe| {
return pe.err; return pe.err;
} }
return .{ .html_document = doc }; return doc;
},
else => {
// Create a new XMLDocument.
const doc = try page._factory.document(XMLDocument{
._proto = undefined,
});
// Parse XML into XMLDocument.
const doc_node = doc.asNode();
var parser = Parser.init(page.arena, doc_node, page);
parser.parseXML(html);
if (parser.err) |pe| {
return pe.err;
}
// If first node is a `ProcessingInstruction`, skip it.
const first_child = doc_node.firstChild() orelse {
// Parsing should fail if there aren't any nodes.
unreachable;
};
if (first_child.getNodeType() == 7) {
// We're sure that firstChild exist, this cannot fail.
_ = doc_node.removeChild(first_child, page) catch unreachable;
}
return .{ .xml_document = doc };
},
};
return error.NotSupported;
} }
pub const JsApi = struct { pub const JsApi = struct {

View File

@@ -79,81 +79,25 @@ pub fn parentNode(self: *DOMTreeWalker) !?*Node {
pub fn firstChild(self: *DOMTreeWalker) !?*Node { pub fn firstChild(self: *DOMTreeWalker) !?*Node {
var node = self._current.firstChild(); var node = self._current.firstChild();
while (node) |n| { while (node) |n| {
const filter_result = try self.acceptNode(n); if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) {
if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = n; self._current = n;
return n; return n;
} }
node = self.nextSiblingOrNull(n);
if (filter_result == NodeFilter.FILTER_SKIP) {
// Descend into children of this skipped node
if (n.firstChild()) |child| {
node = child;
continue;
}
}
// REJECT or SKIP with no children - find next sibling, walking up if necessary
var current_node = n;
while (true) {
if (current_node.nextSibling()) |sibling| {
node = sibling;
break;
}
// No sibling, go up to parent
const parent = current_node._parent orelse return null;
if (parent == self._current) {
// We've exhausted all children of self._current
return null;
}
current_node = parent;
}
} }
return null; return null;
} }
pub fn lastChild(self: *DOMTreeWalker) !?*Node { pub fn lastChild(self: *DOMTreeWalker) !?*Node {
var node = self._current.lastChild(); var node = self._current.lastChild();
while (node) |n| { while (node) |n| {
const filter_result = try self.acceptNode(n); if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) {
if (filter_result == NodeFilter.FILTER_ACCEPT) {
self._current = n; self._current = n;
return n; return n;
} }
node = self.previousSiblingOrNull(n);
if (filter_result == NodeFilter.FILTER_SKIP) {
// Descend into children of this skipped node
if (n.lastChild()) |child| {
node = child;
continue;
}
}
// REJECT or SKIP with no children - find previous sibling, walking up if necessary
var current_node = n;
while (true) {
if (current_node.previousSibling()) |sibling| {
node = sibling;
break;
}
// No sibling, go up to parent
const parent = current_node._parent orelse return null;
if (parent == self._current) {
// We've exhausted all children of self._current
return null;
}
current_node = parent;
}
} }
return null; return null;
} }
@@ -187,39 +131,15 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
var sibling = self.previousSiblingOrNull(node); var sibling = self.previousSiblingOrNull(node);
while (sibling) |sib| { while (sibling) |sib| {
node = sib; node = sib;
var child = self.lastChildOrNull(node);
// Check if this sibling is rejected before descending into it while (child) |c| {
const sib_result = try self.acceptNode(node); if (self.isInSubtree(c)) {
if (sib_result == NodeFilter.FILTER_REJECT) { node = c;
// Skip this sibling and its descendants entirely child = self.lastChildOrNull(node);
sibling = self.previousSiblingOrNull(node); } else {
continue; break;
}
// Descend to the deepest last child, but respect FILTER_REJECT
while (true) {
var child = self.lastChildOrNull(node);
// Find the rightmost non-rejected child
while (child) |c| {
if (!self.isInSubtree(c)) break;
const filter_result = try self.acceptNode(c);
if (filter_result == NodeFilter.FILTER_REJECT) {
// Skip this child and try its previous sibling
child = self.previousSiblingOrNull(c);
} else {
// ACCEPT or SKIP - use this child
break;
}
} }
if (child == null) break; // No acceptable children
// Descend into this child
node = child.?;
} }
if (try self.acceptNode(node) == NodeFilter.FILTER_ACCEPT) { if (try self.acceptNode(node) == NodeFilter.FILTER_ACCEPT) {
self._current = node; self._current = node;
return node; return node;

View File

@@ -48,8 +48,6 @@ _location: ?*Location = null,
_ready_state: ReadyState = .loading, _ready_state: ReadyState = .loading,
_current_script: ?*Element.Html.Script = null, _current_script: ?*Element.Html.Script = null,
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty, _elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty,
// Track IDs that were removed from the map - they might have duplicates in the tree
_removed_ids: std.StringHashMapUnmanaged(void) = .empty,
_active_element: ?*Element = null, _active_element: ?*Element = null,
_style_sheets: ?*StyleSheetList = null, _style_sheets: ?*StyleSheetList = null,
_write_insertion_point: ?*Node = null, _write_insertion_point: ?*Node = null,
@@ -123,15 +121,10 @@ const CreateElementOptions = struct {
is: ?[]const u8 = null, is: ?[]const u8 = null,
}; };
pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element { pub fn createElement(_: *const Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {
const node = try page.createElement(null, name, null); const node = try page.createElement(null, name, null);
const element = node.as(Element); const element = node.as(Element);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
const options = options_ orelse return element; const options = options_ orelse return element;
if (options.is) |is_value| { if (options.is) |is_value| {
try element.setAttribute("is", is_value, page); try element.setAttribute("is", is_value, page);
@@ -141,13 +134,8 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement
return element; return element;
} }
pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element { pub fn createElementNS(_: *const Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {
const node = try page.createElement(namespace, name, null); const node = try page.createElement(namespace, name, null);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node.as(Element); return node.as(Element);
} }
@@ -175,32 +163,9 @@ pub fn createAttributeNS(_: *const Document, namespace: []const u8, name: []cons
}); });
} }
pub fn getElementById(self: *Document, id: []const u8, page: *Page) ?*Element { pub fn getElementById(self: *const Document, id_: ?[]const u8) ?*Element {
if (id.len == 0) { const id = id_ orelse return null;
return null; return self._elements_by_id.get(id);
}
if (self._elements_by_id.get(id)) |element| {
return element;
}
//ID was removed but might have duplicates
if (self._removed_ids.remove(id)) {
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
while (tw.next()) |el| {
const element_id = el.getAttributeSafe("id") orelse continue;
if (std.mem.eql(u8, element_id, id)) {
// we ignore this error to keep getElementById easy to call
// if it really failed, then we're out of memory and nothing's
// going to work like it should anyways.
const owned_id = page.dupeString(id) catch return null;
self._elements_by_id.put(page.arena, owned_id, el) catch return null;
return el;
}
}
}
return null;
} }
const GetElementsByTagNameResult = union(enum) { const GetElementsByTagNameResult = union(enum) {
@@ -287,53 +252,28 @@ pub fn getImplementation(_: *const Document) DOMImplementation {
return .{}; return .{};
} }
pub fn createDocumentFragment(self: *Document, page: *Page) !*Node.DocumentFragment { pub fn createDocumentFragment(_: *const Document, page: *Page) !*Node.DocumentFragment {
const frag = try Node.DocumentFragment.init(page); return Node.DocumentFragment.init(page);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(frag.asNode(), self);
}
return frag;
} }
pub fn createComment(self: *Document, data: []const u8, page: *Page) !*Node { pub fn createComment(_: *const Document, data: []const u8, page: *Page) !*Node {
const node = try page.createComment(data); return page.createComment(data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
} }
pub fn createTextNode(self: *Document, data: []const u8, page: *Page) !*Node { pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node {
const node = try page.createTextNode(data); return page.createTextNode(data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
} }
pub fn createCDATASection(self: *Document, data: []const u8, page: *Page) !*Node { pub fn createCDATASection(self: *const Document, data: []const u8, page: *Page) !*Node {
const node = switch (self._type) { switch (self._type) {
.html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument .html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument
.xml => try page.createCDATASection(data), .xml => return page.createCDATASection(data),
.generic => try page.createCDATASection(data), .generic => return page.createCDATASection(data),
};
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
} }
return node;
} }
pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, page: *Page) !*Node { pub fn createProcessingInstruction(_: *const Document, target: []const u8, data: []const u8, page: *Page) !*Node {
const node = try page.createProcessingInstruction(target, data); return page.createProcessingInstruction(target, data);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node;
} }
const Range = @import("Range.zig"); const Range = @import("Range.zig");
@@ -361,26 +301,14 @@ pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@i
return error.NotSupported; return error.NotSupported;
} }
pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker { pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker {
return DOMTreeWalker.init(root, try whatToShow(what_to_show), filter, page); const show = what_to_show orelse NodeFilter.SHOW_ALL;
return DOMTreeWalker.init(root, show, filter, page);
} }
pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator { pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator {
return DOMNodeIterator.init(root, try whatToShow(what_to_show), filter, page); const show = what_to_show orelse NodeFilter.SHOW_ALL;
} return DOMNodeIterator.init(root, show, filter, page);
fn whatToShow(value_: ?js.Value) !u32 {
const value = value_ orelse return 4294967295; // show all when undefined
if (value.isUndefined()) {
// undefined explicitly passed
return 4294967295;
}
if (value.isNull()) {
return 0;
}
return value.toZig(u32);
} }
pub fn getReadyState(self: *const Document) []const u8 { pub fn getReadyState(self: *const Document) []const u8 {
@@ -745,17 +673,7 @@ pub const JsApi = struct {
pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true }); pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true });
pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{}); pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{});
pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{}); pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{});
pub const getElementById = bridge.function(_getElementById, .{}); pub const getElementById = bridge.function(Document.getElementById, .{});
fn _getElementById(self: *Document, value_: ?js.Value, page: *Page) !?*Element {
const value = value_ orelse return null;
if (value.isNull()) {
return self.getElementById("null", page);
}
if (value.isUndefined()) {
return self.getElementById("undefined", page);
}
return self.getElementById(try value.toZig([]const u8), page);
}
pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true }); pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true });
pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{}); pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{});

View File

@@ -71,10 +71,8 @@ pub fn className(_: *const DocumentFragment) []const u8 {
return "[object DocumentFragment]"; return "[object DocumentFragment]";
} }
pub fn getElementById(self: *DocumentFragment, id: []const u8) ?*Element { pub fn getElementById(self: *DocumentFragment, id_: ?[]const u8) ?*Element {
if (id.len == 0) { const id = id_ orelse return null;
return null;
}
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{}); var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
while (tw.next()) |el| { while (tw.next()) |el| {
@@ -158,12 +156,6 @@ pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText,
const parent_is_connected = parent.isConnected(); const parent_is_connected = parent.isConnected();
for (nodes) |node_or_text| { for (nodes) |node_or_text| {
const child = try node_or_text.toNode(page); const child = try node_or_text.toNode(page);
// If the new children has already a parent, remove from it.
if (child._parent) |p| {
page.removeNode(p, child, .{ .will_be_reconnected = true });
}
try page.appendNode(parent, child, .{ .child_already_connected = parent_is_connected }); try page.appendNode(parent, child, .{ .child_already_connected = parent_is_connected });
} }
} }
@@ -241,18 +233,7 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(DocumentFragment.init, .{}); pub const constructor = bridge.constructor(DocumentFragment.init, .{});
pub const getElementById = bridge.function(_getElementById, .{}); pub const getElementById = bridge.function(DocumentFragment.getElementById, .{});
fn _getElementById(self: *DocumentFragment, value_: ?js.Value) !?*Element {
const value = value_ orelse return null;
if (value.isNull()) {
return self.getElementById("null");
}
if (value.isUndefined()) {
return self.getElementById("undefined");
}
return self.getElementById(try value.toZig([]const u8));
}
pub const querySelector = bridge.function(DocumentFragment.querySelector, .{ .dom_exception = true }); pub const querySelector = bridge.function(DocumentFragment.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(DocumentFragment.querySelectorAll, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(DocumentFragment.querySelectorAll, .{ .dom_exception = true });
pub const children = bridge.accessor(DocumentFragment.getChildren, null, .{}); pub const children = bridge.accessor(DocumentFragment.getChildren, null, .{});

View File

@@ -70,4 +70,9 @@ pub const JsApi = struct {
pub const name = bridge.accessor(DocumentType.getName, null, .{}); pub const name = bridge.accessor(DocumentType.getName, null, .{});
pub const publicId = bridge.accessor(DocumentType.getPublicId, null, .{}); pub const publicId = bridge.accessor(DocumentType.getPublicId, null, .{});
pub const systemId = bridge.accessor(DocumentType.getSystemId, null, .{}); pub const systemId = bridge.accessor(DocumentType.getSystemId, null, .{});
pub const toString = bridge.function(_toString, .{});
fn _toString(self: *const DocumentType) []const u8 {
return self.className();
}
}; };

View File

@@ -44,7 +44,6 @@ const Element = @This();
pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap); pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);
pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties); pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList); pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList);
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot); pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot); pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
@@ -317,20 +316,6 @@ pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {
return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, page); return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, page);
} }
pub fn setOuterHTML(self: *Element, html: []const u8, page: *Page) !void {
const node = self.asNode();
const parent = node._parent orelse return;
page.domChanged();
if (html.len > 0) {
const fragment = (try Node.DocumentFragment.init(page)).asNode();
try page.parseHtmlAsChildren(fragment, html);
try page.insertAllChildrenBefore(fragment, parent, node);
}
page.removeNode(parent, node, .{ .will_be_reconnected = false });
}
pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void { pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {
const dump = @import("../dump.zig"); const dump = @import("../dump.zig");
return dump.children(self.asNode(), .{ .shadow = .skip }, writer, page); return dump.children(self.asNode(), .{ .shadow = .skip }, writer, page);
@@ -565,17 +550,6 @@ pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList {
return gop.value_ptr.*; return gop.value_ptr.*;
} }
pub fn getRelList(self: *Element, page: *Page) !*collections.DOMTokenList {
const gop = try page._element_rel_lists.getOrPut(page.arena, self);
if (!gop.found_existing) {
gop.value_ptr.* = try page._factory.create(collections.DOMTokenList{
._element = self,
._attribute_name = "rel",
});
}
return gop.value_ptr.*;
}
pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap { pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap {
const gop = try page._element_datasets.getOrPut(page.arena, self); const gop = try page._element_datasets.getOrPut(page.arena, self);
if (!gop.found_existing) { if (!gop.found_existing) {
@@ -992,7 +966,7 @@ pub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Pag
var class_names: std.ArrayList([]const u8) = .empty; var class_names: std.ArrayList([]const u8) = .empty;
var it = std.mem.tokenizeAny(u8, class_name, &std.ascii.whitespace); var it = std.mem.tokenizeAny(u8, class_name, &std.ascii.whitespace);
while (it.next()) |name| { while (it.next()) |name| {
try class_names.append(arena, try page.dupeString(name)); try class_names.append(arena, name);
} }
return collections.NodeLive(.class_name).init(self.asNode(), class_names.items, page); return collections.NodeLive(.class_name).init(self.asNode(), class_names.items, page);
@@ -1004,11 +978,6 @@ pub fn cloneElement(self: *Element, deep: bool, page: *Page) !*Node {
const node = try page.createElement(namespace_uri, tag_name, self._attributes); const node = try page.createElement(namespace_uri, tag_name, self._attributes);
// Allow element-specific types to copy their runtime state
_ = Element.Build.call(node.as(Element), "cloned", .{ self, node.as(Element), page }) catch |err| {
log.err(.dom, "element.clone.failed", .{ .err = err });
};
if (deep) { if (deep) {
var child_it = self.asNode().childrenIterator(); var child_it = self.asNode().childrenIterator();
while (child_it.next()) |child| { while (child_it.next()) |child| {
@@ -1239,7 +1208,7 @@ pub const JsApi = struct {
return buf.written(); return buf.written();
} }
pub const outerHTML = bridge.accessor(_outerHTML, Element.setOuterHTML, .{}); pub const outerHTML = bridge.accessor(_outerHTML, null, .{});
fn _outerHTML(self: *Element, page: *Page) ![]const u8 { fn _outerHTML(self: *Element, page: *Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena); var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.getOuterHTML(&buf.writer, page); try self.getOuterHTML(&buf.writer, page);

View File

@@ -40,7 +40,7 @@ _prevent_default: bool = false,
_stop_propagation: bool = false, _stop_propagation: bool = false,
_stop_immediate_propagation: bool = false, _stop_immediate_propagation: bool = false,
_event_phase: EventPhase = .none, _event_phase: EventPhase = .none,
_time_stamp: u64, _time_stamp: u64 = 0,
_needs_retargeting: bool = false, _needs_retargeting: bool = false,
_isTrusted: bool = false, _isTrusted: bool = false,
@@ -105,14 +105,9 @@ pub fn initEvent(
cancelable: ?bool, cancelable: ?bool,
page: *Page, page: *Page,
) !void { ) !void {
if (self._event_phase != .none) {
return;
}
self._type_string = try String.init(page.arena, event_string, .{}); self._type_string = try String.init(page.arena, event_string, .{});
self._bubbles = bubbles orelse false; self._bubbles = bubbles orelse false;
self._cancelable = cancelable orelse false; self._cancelable = cancelable orelse false;
self._stop_propagation = false;
} }
pub fn as(self: *Event, comptime T: type) *T { pub fn as(self: *Event, comptime T: type) *T {
@@ -181,22 +176,6 @@ pub fn getDefaultPrevented(self: *const Event) bool {
return self._prevent_default; return self._prevent_default;
} }
pub fn getReturnValue(self: *const Event) bool {
return !self._prevent_default;
}
pub fn setReturnValue(self: *Event, v: bool) void {
self._prevent_default = !v;
}
pub fn getCancelBubble(self: *const Event) bool {
return self._stop_propagation;
}
pub fn setCancelBubble(self: *Event) void {
self.stopPropagation();
}
pub fn getEventPhase(self: *const Event) u8 { pub fn getEventPhase(self: *const Event) u8 {
return @intFromEnum(self._event_phase); return @intFromEnum(self._event_phase);
} }
@@ -393,7 +372,6 @@ pub const JsApi = struct {
pub const cancelable = bridge.accessor(Event.getCancelable, null, .{}); pub const cancelable = bridge.accessor(Event.getCancelable, null, .{});
pub const composed = bridge.accessor(Event.getComposed, null, .{}); pub const composed = bridge.accessor(Event.getComposed, null, .{});
pub const target = bridge.accessor(Event.getTarget, null, .{}); pub const target = bridge.accessor(Event.getTarget, null, .{});
pub const srcElement = bridge.accessor(Event.getTarget, null, .{});
pub const currentTarget = bridge.accessor(Event.getCurrentTarget, null, .{}); pub const currentTarget = bridge.accessor(Event.getCurrentTarget, null, .{});
pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{}); pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{});
pub const defaultPrevented = bridge.accessor(Event.getDefaultPrevented, null, .{}); pub const defaultPrevented = bridge.accessor(Event.getDefaultPrevented, null, .{});
@@ -404,10 +382,6 @@ pub const JsApi = struct {
pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{}); pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{});
pub const composedPath = bridge.function(Event.composedPath, .{}); pub const composedPath = bridge.function(Event.composedPath, .{});
pub const initEvent = bridge.function(Event.initEvent, .{}); pub const initEvent = bridge.function(Event.initEvent, .{});
// deprecated
pub const returnValue = bridge.accessor(Event.getReturnValue, Event.setReturnValue, .{});
// deprecated
pub const cancelBubble = bridge.accessor(Event.getCancelBubble, Event.setCancelBubble, .{});
// Event phase constants // Event phase constants
pub const NONE = bridge.property(@intFromEnum(EventPhase.none)); pub const NONE = bridge.property(@intFromEnum(EventPhase.none));

View File

@@ -167,6 +167,7 @@ pub const JsApi = struct {
pub const dispatchEvent = bridge.function(EventTarget.dispatchEvent, .{}); pub const dispatchEvent = bridge.function(EventTarget.dispatchEvent, .{});
pub const addEventListener = bridge.function(EventTarget.addEventListener, .{}); pub const addEventListener = bridge.function(EventTarget.addEventListener, .{});
pub const removeEventListener = bridge.function(EventTarget.removeEventListener, .{}); pub const removeEventListener = bridge.function(EventTarget.removeEventListener, .{});
pub const toString = bridge.function(EventTarget.toString, .{});
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

@@ -74,76 +74,30 @@ pub fn getBody(self: *HTMLDocument) ?*Element.Html.Body {
} }
pub fn getTitle(self: *HTMLDocument, page: *Page) ![]const u8 { pub fn getTitle(self: *HTMLDocument, page: *Page) ![]const u8 {
// Search the entire document for the first <title> element const head = self.getHead() orelse return "";
const root = self._proto.getDocumentElement() orelse return ""; var it = head.asNode().childrenIterator();
const title_element = blk: { while (it.next()) |node| {
var walker = @import("TreeWalker.zig").Full.init(root.asNode(), .{}); if (node.is(Element.Html.Title)) |title| {
while (walker.next()) |node| { var buf = std.Io.Writer.Allocating.init(page.call_arena);
if (node.is(Element.Html.Title)) |title| { try title.asElement().getInnerText(&buf.writer);
break :blk title; return buf.written();
}
}
return "";
};
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try title_element.asNode().getTextContent(&buf.writer);
const text = buf.written();
if (text.len == 0) {
return "";
}
var started = false;
var in_whitespace = false;
var result: std.ArrayList(u8) = .empty;
try result.ensureTotalCapacity(page.call_arena, text.len);
for (text) |c| {
const is_ascii_ws = c == ' ' or c == '\t' or c == '\n' or c == '\r' or c == '\x0C';
if (is_ascii_ws) {
if (started) {
in_whitespace = true;
}
} else {
if (in_whitespace) {
result.appendAssumeCapacity(' ');
in_whitespace = false;
}
result.appendAssumeCapacity(c);
started = true;
} }
} }
return "";
return result.items;
} }
pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void { pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void {
const head = self.getHead() orelse return; const head = self.getHead() orelse return;
// Find existing title element in head
var it = head.asNode().childrenIterator(); var it = head.asNode().childrenIterator();
while (it.next()) |node| { while (it.next()) |node| {
if (node.is(Element.Html.Title)) |title_element| { if (node.is(Element.Html.Title)) |title_element| {
// Replace children, but don't create text node for empty string return title_element.asElement().replaceChildren(&.{.{ .text = title }}, page);
if (title.len == 0) {
return title_element.asElement().replaceChildren(&.{}, page);
} else {
return title_element.asElement().replaceChildren(&.{.{ .text = title }}, page);
}
} }
} }
// No title element found, create one
const title_node = try page.createElement(null, "title", null); const title_node = try page.createElement(null, "title", null);
const title_element = title_node.as(Element); const title_element = title_node.as(Element);
try title_element.replaceChildren(&.{.{ .text = title }}, page);
// Only add text if non-empty
if (title.len > 0) {
try title_element.replaceChildren(&.{.{ .text = title }}, page);
}
_ = try head.asNode().appendChild(title_node, page); _ = try head.asNode().appendChild(title_node, page);
} }

View File

@@ -126,7 +126,7 @@ pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
} }
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry { pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
const entries = try page.call_arena.dupe(*IntersectionObserverEntry, self._pending_entries.items); const entries = try page.arena.dupe(*IntersectionObserverEntry, self._pending_entries.items);
self._pending_entries.clearRetainingCapacity(); self._pending_entries.clearRetainingCapacity();
return entries; return entries;
} }

View File

@@ -46,9 +46,6 @@ _parent: ?*Node = null,
_children: ?*Children = null, _children: ?*Children = null,
_child_link: LinkedList.Node = .{}, _child_link: LinkedList.Node = .{},
// Lookup for nodes that have a different owner document than page.document
pub const OwnerDocumentLookup = std.AutoHashMapUnmanaged(*Node, *Document);
pub const Type = union(enum) { pub const Type = union(enum) {
cdata: *CData, cdata: *CData,
element: *Element, element: *Element,
@@ -204,10 +201,6 @@ fn validateNodeInsertion(parent: *Node, node: *Node) !void {
if (node.contains(parent)) { if (node.contains(parent)) {
return error.HierarchyError; return error.HierarchyError;
} }
if (node._type == .attribute) {
return error.HierarchyError;
}
} }
pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node { pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {
@@ -224,11 +217,10 @@ pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {
// then we can remove + add a bit more efficiently (we don't have to fully // then we can remove + add a bit more efficiently (we don't have to fully
// disconnect then reconnect) // disconnect then reconnect)
const child_connected = child.isConnected(); const child_connected = child.isConnected();
// Check if we're adopting the node to a different document // Check if we're adopting the node to a different document
const child_owner = child.ownerDocument(page); const child_root = child.getRootNode(null);
const parent_owner = self.ownerDocument(page) orelse self.as(Document); const parent_root = self.getRootNode(null);
const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner; const adopting_to_new_document = child_connected and child_root != parent_root;
if (child._parent) |parent| { if (child._parent) |parent| {
// we can signal removeNode that the child will remain connected // we can signal removeNode that the child will remain connected
@@ -236,11 +228,6 @@ pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {
page.removeNode(parent, child, .{ .will_be_reconnected = self.isConnected() }); page.removeNode(parent, child, .{ .will_be_reconnected = self.isConnected() });
} }
// Adopt the node tree if moving between documents
if (adopting_to_new_document) {
try page.adoptNodeTree(child, parent_owner);
}
try page.appendNode(self, child, .{ try page.appendNode(self, child, .{
.child_already_connected = child_connected, .child_already_connected = child_connected,
.adopting_to_new_document = adopting_to_new_document, .adopting_to_new_document = adopting_to_new_document,
@@ -440,13 +427,8 @@ pub fn ownerDocument(self: *const Node, page: *const Page) ?*Document {
return current._type.document; return current._type.document;
} }
// Otherwise, this is a detached node. Check if it has a specific owner // Otherwise, this is a detached node. The owner is the document that
// document registered (for nodes created via non-main documents). // created it. For now, we only have one document.
if (page._node_owner_documents.get(@constCast(self))) |owner| {
return owner;
}
// Default to the main document for detached nodes without a specific owner.
return page.document; return page.document;
} }
@@ -475,21 +457,6 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page
return self.appendChild(new_node, page); return self.appendChild(new_node, page);
}; };
// special case: if nodes are the same, ignore the change.
if (new_node == ref_node_) {
page.domChanged();
if (page.hasMutationObservers()) {
const parent = new_node._parent.?;
const previous_sibling = new_node.previousSibling();
const next_sibling = new_node.nextSibling();
const replaced = [_]*Node{new_node};
page.childListChange(parent, &replaced, &replaced, previous_sibling, next_sibling);
}
return new_node;
}
if (ref_node._parent == null or ref_node._parent.? != self) { if (ref_node._parent == null or ref_node._parent.? != self) {
return error.NotFound; return error.NotFound;
} }
@@ -502,11 +469,10 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page
try validateNodeInsertion(self, new_node); try validateNodeInsertion(self, new_node);
const child_already_connected = new_node.isConnected(); const child_already_connected = new_node.isConnected();
// Check if we're adopting the node to a different document // Check if we're adopting the node to a different document
const child_owner = new_node.ownerDocument(page); const child_root = new_node.getRootNode(null);
const parent_owner = self.ownerDocument(page) orelse self.as(Document); const parent_root = self.getRootNode(null);
const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner; const adopting_to_new_document = child_already_connected and child_root != parent_root;
page.domChanged(); page.domChanged();
const will_be_reconnected = self.isConnected(); const will_be_reconnected = self.isConnected();
@@ -514,11 +480,6 @@ pub fn insertBefore(self: *Node, new_node: *Node, ref_node_: ?*Node, page: *Page
page.removeNode(parent, new_node, .{ .will_be_reconnected = will_be_reconnected }); page.removeNode(parent, new_node, .{ .will_be_reconnected = will_be_reconnected });
} }
// Adopt the node tree if moving between documents
if (adopting_to_new_document) {
try page.adoptNodeTree(new_node, parent_owner);
}
try page.insertNodeRelative( try page.insertNodeRelative(
self, self,
new_node, new_node,
@@ -540,13 +501,7 @@ pub fn replaceChild(self: *Node, new_child: *Node, old_child: *Node, page: *Page
try validateNodeInsertion(self, new_child); try validateNodeInsertion(self, new_child);
_ = try self.insertBefore(new_child, old_child, page); _ = try self.insertBefore(new_child, old_child, page);
page.removeNode(self, old_child, .{ .will_be_reconnected = false });
// Special case: if we replace a node by itself, we don't remove it.
// insertBefore is an noop in this case.
if (new_child != old_child) {
page.removeNode(self, old_child, .{ .will_be_reconnected = false });
}
return old_child; return old_child;
} }
@@ -871,14 +826,11 @@ pub const JsApi = struct {
pub const ATTRIBUTE_NODE = bridge.property(2); pub const ATTRIBUTE_NODE = bridge.property(2);
pub const TEXT_NODE = bridge.property(3); pub const TEXT_NODE = bridge.property(3);
pub const CDATA_SECTION_NODE = bridge.property(4); pub const CDATA_SECTION_NODE = bridge.property(4);
pub const ENTITY_REFERENCE_NODE = bridge.property(5);
pub const ENTITY_NODE = bridge.property(6);
pub const PROCESSING_INSTRUCTION_NODE = bridge.property(7); pub const PROCESSING_INSTRUCTION_NODE = bridge.property(7);
pub const COMMENT_NODE = bridge.property(8); pub const COMMENT_NODE = bridge.property(8);
pub const DOCUMENT_NODE = bridge.property(9); pub const DOCUMENT_NODE = bridge.property(9);
pub const DOCUMENT_TYPE_NODE = bridge.property(10); pub const DOCUMENT_TYPE_NODE = bridge.property(10);
pub const DOCUMENT_FRAGMENT_NODE = bridge.property(11); pub const DOCUMENT_FRAGMENT_NODE = bridge.property(11);
pub const NOTATION_NODE = bridge.property(12);
pub const DOCUMENT_POSITION_DISCONNECTED = bridge.property(0x01); pub const DOCUMENT_POSITION_DISCONNECTED = bridge.property(0x01);
pub const DOCUMENT_POSITION_PRECEDING = bridge.property(0x02); pub const DOCUMENT_POSITION_PRECEDING = bridge.property(0x02);
@@ -934,6 +886,11 @@ pub const JsApi = struct {
pub const getRootNode = bridge.function(Node.getRootNode, .{}); pub const getRootNode = bridge.function(Node.getRootNode, .{});
pub const isEqualNode = bridge.function(Node.isEqualNode, .{}); pub const isEqualNode = bridge.function(Node.isEqualNode, .{});
pub const toString = bridge.function(_toString, .{});
fn _toString(self: *const Node) []const u8 {
return self.className();
}
fn _baseURI(_: *Node, page: *const Page) []const u8 { fn _baseURI(_: *Node, page: *const Page) []const u8 {
return page.base(); return page.base();
} }

View File

@@ -124,7 +124,7 @@ pub fn disconnect(self: *PerformanceObserver, page: *Page) void {
/// Returns the current list of PerformanceEntry objects /// Returns the current list of PerformanceEntry objects
/// stored in the performance observer, emptying it out. /// stored in the performance observer, emptying it out.
pub fn takeRecords(self: *PerformanceObserver, page: *Page) ![]*Performance.Entry { pub fn takeRecords(self: *PerformanceObserver, page: *Page) ![]*Performance.Entry {
const records = try page.call_arena.dupe(*Performance.Entry, self._entries.items); const records = try page.arena.dupe(*Performance.Entry, self._entries.items);
self._entries.clearRetainingCapacity(); self._entries.clearRetainingCapacity();
return records; return records;
} }

View File

@@ -37,35 +37,22 @@ pub fn init(page: *Page) !*Range {
} }
pub fn setStart(self: *Range, node: *Node, offset: u32) !void { pub fn setStart(self: *Range, node: *Node, offset: u32) !void {
if (offset > node.getLength()) {
return error.IndexSizeError;
}
self._proto._start_container = node; self._proto._start_container = node;
self._proto._start_offset = offset; self._proto._start_offset = offset;
// If start is now after end, or nodes are in different trees, collapse to start // If start is now after end, collapse to start
const end_root = self._proto._end_container.getRootNode(null); if (self._proto.isStartAfterEnd()) {
const start_root = node.getRootNode(null);
if (end_root != start_root or self._proto.isStartAfterEnd()) {
self._proto._end_container = self._proto._start_container; self._proto._end_container = self._proto._start_container;
self._proto._end_offset = self._proto._start_offset; self._proto._end_offset = self._proto._start_offset;
} }
} }
pub fn setEnd(self: *Range, node: *Node, offset: u32) !void { pub fn setEnd(self: *Range, node: *Node, offset: u32) !void {
// Validate offset
if (offset > node.getLength()) {
return error.IndexSizeError;
}
self._proto._end_container = node; self._proto._end_container = node;
self._proto._end_offset = offset; self._proto._end_offset = offset;
// If end is now before start, or nodes are in different trees, collapse to end // If end is now before start, collapse to end
const start_root = self._proto._start_container.getRootNode(null); if (self._proto.isStartAfterEnd()) {
const end_root = node.getRootNode(null);
if (start_root != end_root or self._proto.isStartAfterEnd()) {
self._proto._start_container = self._proto._end_container; self._proto._start_container = self._proto._end_container;
self._proto._start_offset = self._proto._end_offset; self._proto._start_offset = self._proto._end_offset;
} }
@@ -118,181 +105,6 @@ pub fn collapse(self: *Range, to_start: ?bool) void {
} }
} }
pub fn detach(_: *Range) void {
// Legacy no-op method kept for backwards compatibility
// Modern spec: "The detach() method must do nothing."
}
pub fn compareBoundaryPoints(self: *const Range, how_raw: i32, source_range: *const Range) !i16 {
// Convert how parameter per WebIDL unsigned short conversion
// This handles negative numbers and out-of-range values
const how_mod = @mod(how_raw, 65536);
const how: u16 = if (how_mod < 0) @intCast(@as(i32, how_mod) + 65536) else @intCast(how_mod);
// If how is not one of 0, 1, 2, or 3, throw NotSupportedError
if (how > 3) {
return error.NotSupported;
}
// If the two ranges' root is different, throw WrongDocumentError
const this_root = self._proto._start_container.getRootNode(null);
const source_root = source_range._proto._start_container.getRootNode(null);
if (this_root != source_root) {
return error.WrongDocument;
}
// Determine which boundary points to compare based on how parameter
const result = switch (how) {
0 => AbstractRange.compareBoundaryPoints( // START_TO_START
self._proto._start_container,
self._proto._start_offset,
source_range._proto._start_container,
source_range._proto._start_offset,
),
1 => AbstractRange.compareBoundaryPoints( // START_TO_END
self._proto._start_container,
self._proto._start_offset,
source_range._proto._end_container,
source_range._proto._end_offset,
),
2 => AbstractRange.compareBoundaryPoints( // END_TO_END
self._proto._end_container,
self._proto._end_offset,
source_range._proto._end_container,
source_range._proto._end_offset,
),
3 => AbstractRange.compareBoundaryPoints( // END_TO_START
self._proto._end_container,
self._proto._end_offset,
source_range._proto._start_container,
source_range._proto._start_offset,
),
else => unreachable,
};
return switch (result) {
.before => -1,
.equal => 0,
.after => 1,
};
}
pub fn comparePoint(self: *const Range, node: *Node, offset: u32) !i16 {
if (offset > node.getLength()) {
return error.IndexSizeError;
}
// Check if node is in a different tree than the range
const node_root = node.getRootNode(null);
const start_root = self._proto._start_container.getRootNode(null);
if (node_root != start_root) {
return error.WrongDocument;
}
// Compare point with start boundary
const cmp_start = AbstractRange.compareBoundaryPoints(
node,
offset,
self._proto._start_container,
self._proto._start_offset,
);
if (cmp_start == .before) {
return -1;
}
const cmp_end = AbstractRange.compareBoundaryPoints(
node,
offset,
self._proto._end_container,
self._proto._end_offset,
);
return if (cmp_end == .after) 1 else 0;
}
pub fn isPointInRange(self: *const Range, node: *Node, offset: u32) !bool {
// If node's root is different from the context object's root, return false
const node_root = node.getRootNode(null);
const start_root = self._proto._start_container.getRootNode(null);
if (node_root != start_root) {
return false;
}
if (node._type == .document_type) {
return error.InvalidNodeType;
}
// If offset is greater than node's length, throw IndexSizeError
if (offset > node.getLength()) {
return error.IndexSizeError;
}
// If (node, offset) is before start or after end, return false
const cmp_start = AbstractRange.compareBoundaryPoints(
node,
offset,
self._proto._start_container,
self._proto._start_offset,
);
if (cmp_start == .before) {
return false;
}
const cmp_end = AbstractRange.compareBoundaryPoints(
node,
offset,
self._proto._end_container,
self._proto._end_offset,
);
return cmp_end != .after;
}
pub fn intersectsNode(self: *const Range, node: *Node) bool {
// If node's root is different from the context object's root, return false
const node_root = node.getRootNode(null);
const start_root = self._proto._start_container.getRootNode(null);
if (node_root != start_root) {
return false;
}
// Let parent be node's parent
const parent = node.parentNode() orelse {
// If parent is null, return true
return true;
};
// Let offset be node's index
const offset = parent.getChildIndex(node) orelse {
// Should not happen if node has a parent
return false;
};
// If (parent, offset) is before end and (parent, offset + 1) is after start, return true
const before_end = AbstractRange.compareBoundaryPoints(
parent,
offset,
self._proto._end_container,
self._proto._end_offset,
);
const after_start = AbstractRange.compareBoundaryPoints(
parent,
offset + 1,
self._proto._start_container,
self._proto._start_offset,
);
if (before_end == .before and after_start == .after) {
return true;
}
// Return false
return false;
}
pub fn cloneRange(self: *const Range, page: *Page) !*Range { pub fn cloneRange(self: *const Range, page: *Page) !*Range {
const clone = try page._factory.abstractRange(Range{ ._proto = undefined }, page); const clone = try page._factory.abstractRange(Range{ ._proto = undefined }, page);
clone._proto._end_offset = self._proto._end_offset; clone._proto._end_offset = self._proto._end_offset;
@@ -496,35 +308,24 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
}; };
// Constants for compareBoundaryPoints
pub const START_TO_START = bridge.property(0);
pub const START_TO_END = bridge.property(1);
pub const END_TO_END = bridge.property(2);
pub const END_TO_START = bridge.property(3);
pub const constructor = bridge.constructor(Range.init, .{}); pub const constructor = bridge.constructor(Range.init, .{});
pub const setStart = bridge.function(Range.setStart, .{ .dom_exception = true }); pub const setStart = bridge.function(Range.setStart, .{});
pub const setEnd = bridge.function(Range.setEnd, .{ .dom_exception = true }); pub const setEnd = bridge.function(Range.setEnd, .{});
pub const setStartBefore = bridge.function(Range.setStartBefore, .{ .dom_exception = true }); pub const setStartBefore = bridge.function(Range.setStartBefore, .{});
pub const setStartAfter = bridge.function(Range.setStartAfter, .{ .dom_exception = true }); pub const setStartAfter = bridge.function(Range.setStartAfter, .{});
pub const setEndBefore = bridge.function(Range.setEndBefore, .{ .dom_exception = true }); pub const setEndBefore = bridge.function(Range.setEndBefore, .{});
pub const setEndAfter = bridge.function(Range.setEndAfter, .{ .dom_exception = true }); pub const setEndAfter = bridge.function(Range.setEndAfter, .{});
pub const selectNode = bridge.function(Range.selectNode, .{ .dom_exception = true }); pub const selectNode = bridge.function(Range.selectNode, .{});
pub const selectNodeContents = bridge.function(Range.selectNodeContents, .{}); pub const selectNodeContents = bridge.function(Range.selectNodeContents, .{});
pub const collapse = bridge.function(Range.collapse, .{ .dom_exception = true }); pub const collapse = bridge.function(Range.collapse, .{});
pub const detach = bridge.function(Range.detach, .{}); pub const cloneRange = bridge.function(Range.cloneRange, .{});
pub const compareBoundaryPoints = bridge.function(Range.compareBoundaryPoints, .{ .dom_exception = true }); pub const insertNode = bridge.function(Range.insertNode, .{});
pub const comparePoint = bridge.function(Range.comparePoint, .{ .dom_exception = true }); pub const deleteContents = bridge.function(Range.deleteContents, .{});
pub const isPointInRange = bridge.function(Range.isPointInRange, .{ .dom_exception = true }); pub const cloneContents = bridge.function(Range.cloneContents, .{});
pub const intersectsNode = bridge.function(Range.intersectsNode, .{}); pub const extractContents = bridge.function(Range.extractContents, .{});
pub const cloneRange = bridge.function(Range.cloneRange, .{ .dom_exception = true }); pub const surroundContents = bridge.function(Range.surroundContents, .{});
pub const insertNode = bridge.function(Range.insertNode, .{ .dom_exception = true }); pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{});
pub const deleteContents = bridge.function(Range.deleteContents, .{ .dom_exception = true }); pub const toString = bridge.function(Range.toString, .{});
pub const cloneContents = bridge.function(Range.cloneContents, .{ .dom_exception = true });
pub const extractContents = bridge.function(Range.extractContents, .{ .dom_exception = true });
pub const surroundContents = bridge.function(Range.surroundContents, .{ .dom_exception = true });
pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{ .dom_exception = true });
pub const toString = bridge.function(Range.toString, .{ .dom_exception = true });
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

@@ -39,7 +39,6 @@ _proto: *DocumentFragment,
_mode: Mode, _mode: Mode,
_host: *Element, _host: *Element,
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .{}, _elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},
_removed_ids: std.StringHashMapUnmanaged(void) = .{},
pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot { pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot {
return page._factory.documentFragment(ShadowRoot{ return page._factory.documentFragment(ShadowRoot{
@@ -73,34 +72,9 @@ pub fn getHost(self: *const ShadowRoot) *Element {
return self._host; return self._host;
} }
pub fn getElementById(self: *ShadowRoot, id: []const u8, page: *Page) ?*Element { pub fn getElementById(self: *ShadowRoot, id_: ?[]const u8) ?*Element {
if (id.len == 0) { const id = id_ orelse return null;
return null; return self._elements_by_id.get(id);
}
// Fast path: ID is in the map
if (self._elements_by_id.get(id)) |element| {
return element;
}
// Slow path: ID was removed but might have duplicates
if (self._removed_ids.remove(id)) {
// Do a tree walk to find another element with this ID
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
while (tw.next()) |el| {
const element_id = el.getAttributeSafe("id") orelse continue;
if (std.mem.eql(u8, element_id, id)) {
// we ignore this error to keep getElementById easy to call
// if it really failed, then we're out of memory and nothing's
// going to work like it should anyways.
const owned_id = page.dupeString(id) catch return null;
self._elements_by_id.put(page.arena, owned_id, el) catch return null;
return el;
}
}
}
return null;
} }
pub const JsApi = struct { pub const JsApi = struct {
@@ -114,17 +88,7 @@ pub const JsApi = struct {
pub const mode = bridge.accessor(ShadowRoot.getMode, null, .{}); pub const mode = bridge.accessor(ShadowRoot.getMode, null, .{});
pub const host = bridge.accessor(ShadowRoot.getHost, null, .{}); pub const host = bridge.accessor(ShadowRoot.getHost, null, .{});
pub const getElementById = bridge.function(_getElementById, .{}); pub const getElementById = bridge.function(ShadowRoot.getElementById, .{});
fn _getElementById(self: *ShadowRoot, value_: ?js.Value, page: *Page) !?*Element {
const value = value_ orelse return null;
if (value.isNull()) {
return self.getElementById("null", page);
}
if (value.isUndefined()) {
return self.getElementById("undefined", page);
}
return self.getElementById(try value.toZig([]const u8), page);
}
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

@@ -28,6 +28,7 @@ const Mode = enum {
tag, tag,
tag_name, tag_name,
class_name, class_name,
name,
all_elements, all_elements,
child_elements, child_elements,
child_tag, child_tag,
@@ -43,6 +44,7 @@ _data: union(Mode) {
tag: NodeLive(.tag), tag: NodeLive(.tag),
tag_name: NodeLive(.tag_name), tag_name: NodeLive(.tag_name),
class_name: NodeLive(.class_name), class_name: NodeLive(.class_name),
name: NodeLive(.name),
all_elements: NodeLive(.all_elements), all_elements: NodeLive(.all_elements),
child_elements: NodeLive(.child_elements), child_elements: NodeLive(.child_elements),
child_tag: NodeLive(.child_tag), child_tag: NodeLive(.child_tag),
@@ -77,6 +79,7 @@ pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {
.tag => |*impl| .{ .tag = impl._tw.clone() }, .tag => |*impl| .{ .tag = impl._tw.clone() },
.tag_name => |*impl| .{ .tag_name = impl._tw.clone() }, .tag_name => |*impl| .{ .tag_name = impl._tw.clone() },
.class_name => |*impl| .{ .class_name = impl._tw.clone() }, .class_name => |*impl| .{ .class_name = impl._tw.clone() },
.name => |*impl| .{ .name = impl._tw.clone() },
.all_elements => |*impl| .{ .all_elements = impl._tw.clone() }, .all_elements => |*impl| .{ .all_elements = impl._tw.clone() },
.child_elements => |*impl| .{ .child_elements = impl._tw.clone() }, .child_elements => |*impl| .{ .child_elements = impl._tw.clone() },
.child_tag => |*impl| .{ .child_tag = impl._tw.clone() }, .child_tag => |*impl| .{ .child_tag = impl._tw.clone() },
@@ -95,6 +98,7 @@ pub const Iterator = GenericIterator(struct {
tag: TreeWalker.FullExcludeSelf, tag: TreeWalker.FullExcludeSelf,
tag_name: TreeWalker.FullExcludeSelf, tag_name: TreeWalker.FullExcludeSelf,
class_name: TreeWalker.FullExcludeSelf, class_name: TreeWalker.FullExcludeSelf,
name: TreeWalker.FullExcludeSelf,
all_elements: TreeWalker.FullExcludeSelf, all_elements: TreeWalker.FullExcludeSelf,
child_elements: TreeWalker.Children, child_elements: TreeWalker.Children,
child_tag: TreeWalker.Children, child_tag: TreeWalker.Children,
@@ -109,6 +113,7 @@ pub const Iterator = GenericIterator(struct {
.tag => |*impl| impl.nextTw(&self.tw.tag), .tag => |*impl| impl.nextTw(&self.tw.tag),
.tag_name => |*impl| impl.nextTw(&self.tw.tag_name), .tag_name => |*impl| impl.nextTw(&self.tw.tag_name),
.class_name => |*impl| impl.nextTw(&self.tw.class_name), .class_name => |*impl| impl.nextTw(&self.tw.class_name),
.name => |*impl| impl.nextTw(&self.tw.name),
.all_elements => |*impl| impl.nextTw(&self.tw.all_elements), .all_elements => |*impl| impl.nextTw(&self.tw.all_elements),
.child_elements => |*impl| impl.nextTw(&self.tw.child_elements), .child_elements => |*impl| impl.nextTw(&self.tw.child_elements),
.child_tag => |*impl| impl.nextTw(&self.tw.child_tag), .child_tag => |*impl| impl.nextTw(&self.tw.child_tag),

View File

@@ -26,13 +26,11 @@ const Node = @import("../Node.zig");
const ChildNodes = @import("ChildNodes.zig"); const ChildNodes = @import("ChildNodes.zig");
const RadioNodeList = @import("RadioNodeList.zig"); const RadioNodeList = @import("RadioNodeList.zig");
const SelectorList = @import("../selector/List.zig"); const SelectorList = @import("../selector/List.zig");
const NodeLive = @import("node_live.zig").NodeLive;
const Mode = enum { const Mode = enum {
child_nodes, child_nodes,
selector_list, selector_list,
radio_node_list, radio_node_list,
name,
}; };
const NodeList = @This(); const NodeList = @This();
@@ -41,7 +39,6 @@ data: union(Mode) {
child_nodes: *ChildNodes, child_nodes: *ChildNodes,
selector_list: *SelectorList, selector_list: *SelectorList,
radio_node_list: *RadioNodeList, radio_node_list: *RadioNodeList,
name: NodeLive(.name),
}, },
pub fn length(self: *NodeList, page: *Page) !u32 { pub fn length(self: *NodeList, page: *Page) !u32 {
@@ -49,7 +46,6 @@ pub fn length(self: *NodeList, page: *Page) !u32 {
.child_nodes => |impl| impl.length(page), .child_nodes => |impl| impl.length(page),
.selector_list => |impl| @intCast(impl.getLength()), .selector_list => |impl| @intCast(impl.getLength()),
.radio_node_list => |impl| impl.getLength(), .radio_node_list => |impl| impl.getLength(),
.name => |*impl| impl.length(page),
}; };
} }
@@ -58,7 +54,6 @@ pub fn getAtIndex(self: *NodeList, index: usize, page: *Page) !?*Node {
.child_nodes => |impl| impl.getAtIndex(index, page), .child_nodes => |impl| impl.getAtIndex(index, page),
.selector_list => |impl| impl.getAtIndex(index), .selector_list => |impl| impl.getAtIndex(index),
.radio_node_list => |impl| impl.getAtIndex(index, page), .radio_node_list => |impl| impl.getAtIndex(index, page),
.name => |*impl| if (impl.getAtIndex(index, page)) |el| el.asNode() else null,
}; };
} }

View File

@@ -171,7 +171,7 @@ pub fn NodeLive(comptime mode: Mode) type {
} }
pub fn getByName(self: *Self, name: []const u8, page: *Page) ?*Element { pub fn getByName(self: *Self, name: []const u8, page: *Page) ?*Element {
if (page.document.getElementById(name, page)) |element| { if (page.document.getElementById(name)) |element| {
const node = element.asNode(); const node = element.asNode();
if (self._tw.contains(node) and self.matches(node)) { if (self._tw.contains(node) and self.matches(node)) {
return element; return element;
@@ -320,14 +320,12 @@ pub fn NodeLive(comptime mode: Mode) type {
} }
const HTMLCollection = @import("HTMLCollection.zig"); const HTMLCollection = @import("HTMLCollection.zig");
const NodeList = @import("NodeList.zig"); pub fn runtimeGenericWrap(self: Self, page: *Page) !*HTMLCollection {
pub fn runtimeGenericWrap(self: Self, page: *Page) !if (mode == .name) *NodeList else *HTMLCollection {
const collection = switch (mode) { const collection = switch (mode) {
.name => return page._factory.create(NodeList{ .data = .{ .name = self } }),
.tag => HTMLCollection{ ._data = .{ .tag = self } }, .tag => HTMLCollection{ ._data = .{ .tag = self } },
.tag_name => HTMLCollection{ ._data = .{ .tag_name = self } }, .tag_name => HTMLCollection{ ._data = .{ .tag_name = self } },
.class_name => HTMLCollection{ ._data = .{ .class_name = self } }, .class_name => HTMLCollection{ ._data = .{ .class_name = self } },
.name => HTMLCollection{ ._data = .{ .name = self } },
.all_elements => HTMLCollection{ ._data = .{ .all_elements = self } }, .all_elements => HTMLCollection{ ._data = .{ .all_elements = self } },
.child_elements => HTMLCollection{ ._data = .{ .child_elements = self } }, .child_elements => HTMLCollection{ ._data = .{ .child_elements = self } },
.child_tag => HTMLCollection{ ._data = .{ .child_tag = self } }, .child_tag => HTMLCollection{ ._data = .{ .child_tag = self } },

View File

@@ -20,8 +20,6 @@ const std = @import("std");
const log = @import("../../../log.zig"); const log = @import("../../../log.zig");
const String = @import("../../../string.zig").String; const String = @import("../../../string.zig").String;
const CssParser = @import("../../css/Parser.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Element = @import("../Element.zig"); const Element = @import("../Element.zig");
@@ -151,10 +149,30 @@ pub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, page: *Page) !vo
} }
// Parse and set new properties // Parse and set new properties
var it = CssParser.parseDeclarationsList(text); // This is a simple parser - a full implementation would use a proper CSS parser
var it = std.mem.splitScalar(u8, text, ';');
while (it.next()) |declaration| { while (it.next()) |declaration| {
const priority: ?[]const u8 = if (declaration.important) "important" else null; const trimmed = std.mem.trim(u8, declaration, &std.ascii.whitespace);
try self.setProperty(declaration.name, declaration.value, priority, page); if (trimmed.len == 0) continue;
if (std.mem.indexOfScalar(u8, trimmed, ':')) |colon_pos| {
const name = std.mem.trim(u8, trimmed[0..colon_pos], &std.ascii.whitespace);
const value_part = std.mem.trim(u8, trimmed[colon_pos + 1 ..], &std.ascii.whitespace);
var value = value_part;
var priority: ?[]const u8 = null;
// Check for !important
if (std.mem.lastIndexOfScalar(u8, value_part, '!')) |bang_pos| {
const after_bang = std.mem.trim(u8, value_part[bang_pos + 1 ..], &std.ascii.whitespace);
if (std.mem.eql(u8, after_bang, "important")) {
value = std.mem.trimRight(u8, value_part[0..bang_pos], &std.ascii.whitespace);
priority = "important";
}
}
try self.setProperty(name, value, priority, page);
}
} }
} }

View File

@@ -477,19 +477,11 @@ pub const NamedNodeMap = struct {
return self._list.getAttribute(name, self._element, page); return self._list.getAttribute(name, self._element, page);
} }
pub fn set(self: *const NamedNodeMap, attribute: *Attribute, page: *Page) !?*Attribute { pub fn setByName(self: *const NamedNodeMap, attribute: *Attribute, page: *Page) !?*Attribute {
attribute._element = null; // just a requirement of list.putAttribute, it'll re-set it. attribute._element = null; // just a requirement of list.putAttribute, it'll re-set it.
return self._list.putAttribute(attribute, self._element, page); return self._list.putAttribute(attribute, self._element, page);
} }
pub fn removeByName(self: *const NamedNodeMap, name: []const u8, page: *Page) !?*Attribute {
// this 2-step process (get then delete) isn't efficient. But we don't
// expect this to be called often, and this lets us keep delete straightforward.
const attr = (try self.getByName(name, page)) orelse return null;
try self._list.delete(name, self._element, page);
return attr;
}
pub fn iterator(self: *const NamedNodeMap, page: *Page) !*Iterator { pub fn iterator(self: *const NamedNodeMap, page: *Page) !*Iterator {
return .init(.{ .list = self }, page); return .init(.{ .list = self }, page);
} }
@@ -518,8 +510,7 @@ pub const NamedNodeMap = struct {
pub const @"[int]" = bridge.indexed(NamedNodeMap.getAtIndex, .{ .null_as_undefined = true }); pub const @"[int]" = bridge.indexed(NamedNodeMap.getAtIndex, .{ .null_as_undefined = true });
pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, null, null, .{ .null_as_undefined = true }); pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, null, null, .{ .null_as_undefined = true });
pub const getNamedItem = bridge.function(NamedNodeMap.getByName, .{}); pub const getNamedItem = bridge.function(NamedNodeMap.getByName, .{});
pub const setNamedItem = bridge.function(NamedNodeMap.set, .{}); pub const setNamedItem = bridge.function(NamedNodeMap.setByName, .{});
pub const removeNamedItem = bridge.function(NamedNodeMap.removeByName, .{});
pub const item = bridge.function(_item, .{}); pub const item = bridge.function(_item, .{});
fn _item(self: *const NamedNodeMap, index: i32, page: *Page) !?*Attribute { fn _item(self: *const NamedNodeMap, index: i32, page: *Page) !?*Attribute {
// the bridge.indexed handles this, so if we want // the bridge.indexed handles this, so if we want

View File

@@ -220,18 +220,7 @@ pub const JsApi = struct {
pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{}); pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{});
pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{}); pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{});
pub const text = bridge.accessor(Anchor.getText, Anchor.setText, .{}); pub const text = bridge.accessor(Anchor.getText, Anchor.setText, .{});
pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true });
pub const toString = bridge.function(Anchor.getHref, .{}); pub const toString = bridge.function(Anchor.getHref, .{});
fn _getRelList(self: *Anchor, page: *Page) !?*@import("../../collections.zig").DOMTokenList {
const element = self.asElement();
// relList is only valid for HTML and SVG <a> elements
const namespace = element._namespace;
if (namespace != .html and namespace != .svg) {
return null;
}
return element.getRelList(page);
}
}; };
const testing = @import("../../../../testing.zig"); const testing = @import("../../../../testing.zig");

View File

@@ -91,7 +91,7 @@ pub fn getForm(self: *Button, page: *Page) ?*Form {
// If form attribute exists, ONLY use that (even if it references nothing) // If form attribute exists, ONLY use that (even if it references nothing)
if (element.getAttributeSafe("form")) |form_id| { if (element.getAttributeSafe("form")) |form_id| {
if (page.document.getElementById(form_id, page)) |form_element| { if (page.document.getElementById(form_id)) |form_element| {
return form_element.is(Form); return form_element.is(Form);
} }
// form attribute present but invalid - no form owner // form attribute present but invalid - no form owner

View File

@@ -46,16 +46,14 @@ pub const Type = enum {
range, range,
date, date,
time, time,
@"datetime-local", datetime_local,
month, month,
week, week,
color, color,
pub fn fromString(str: []const u8) Type { pub fn fromString(str: []const u8) Type {
// Longest type name is "datetime-local" at 14 chars // Longest type name is "datetime-local" at 14 chars
if (str.len > 32) { if (str.len > 32) return .text;
return .text;
}
var buf: [32]u8 = undefined; var buf: [32]u8 = undefined;
const lower = std.ascii.lowerString(&buf, str); const lower = std.ascii.lowerString(&buf, str);
@@ -63,7 +61,10 @@ pub const Type = enum {
} }
pub fn toString(self: Type) []const u8 { pub fn toString(self: Type) []const u8 {
return @tagName(self); return switch (self) {
.datetime_local => "datetime-local",
else => @tagName(self),
};
} }
}; };
@@ -75,7 +76,6 @@ _checked: bool = false,
_checked_dirty: bool = false, _checked_dirty: bool = false,
_input_type: Type = .text, _input_type: Type = .text,
_selected: bool = false, _selected: bool = false,
_indeterminate: bool = false,
pub fn asElement(self: *Input) *Element { pub fn asElement(self: *Input) *Element {
return self._proto._proto; return self._proto._proto;
@@ -129,14 +129,6 @@ pub fn setChecked(self: *Input, checked: bool, page: *Page) !void {
self._checked_dirty = true; self._checked_dirty = true;
} }
pub fn getIndeterminate(self: *const Input) bool {
return self._indeterminate;
}
pub fn setIndeterminate(self: *Input, value: bool) !void {
self._indeterminate = value;
}
pub fn getDefaultChecked(self: *const Input) bool { pub fn getDefaultChecked(self: *const Input) bool {
return self._default_checked; return self._default_checked;
} }
@@ -264,7 +256,7 @@ pub fn getForm(self: *Input, page: *Page) ?*Form {
// If form attribute exists, ONLY use that (even if it references nothing) // If form attribute exists, ONLY use that (even if it references nothing)
if (element.getAttributeSafe("form")) |form_id| { if (element.getAttributeSafe("form")) |form_id| {
if (page.document.getElementById(form_id, page)) |form_element| { if (page.document.getElementById(form_id)) |form_element| {
return form_element.is(Form); return form_element.is(Form);
} }
// form attribute present but invalid - no form owner // form attribute present but invalid - no form owner
@@ -350,7 +342,6 @@ pub const JsApi = struct {
pub const size = bridge.accessor(Input.getSize, Input.setSize, .{}); pub const size = bridge.accessor(Input.getSize, Input.setSize, .{});
pub const src = bridge.accessor(Input.getSrc, Input.setSrc, .{}); pub const src = bridge.accessor(Input.getSrc, Input.setSrc, .{});
pub const form = bridge.accessor(Input.getForm, null, .{}); pub const form = bridge.accessor(Input.getForm, null, .{});
pub const indeterminate = bridge.accessor(Input.getIndeterminate, Input.setIndeterminate, .{});
pub const select = bridge.function(Input.select, .{}); pub const select = bridge.function(Input.select, .{});
}; };
@@ -413,18 +404,6 @@ pub const Build = struct {
}, },
} }
} }
pub fn cloned(source_element: *Element, cloned_element: *Element, _: *Page) !void {
const source = source_element.as(Input);
const clone = cloned_element.as(Input);
// Copy runtime state from source to clone
clone._value = source._value;
clone._checked = source._checked;
clone._checked_dirty = source._checked_dirty;
clone._selected = source._selected;
clone._indeterminate = source._indeterminate;
}
}; };
const testing = @import("../../../../testing.zig"); const testing = @import("../../../../testing.zig");

View File

@@ -68,16 +68,6 @@ pub const JsApi = struct {
pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{}); pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{});
pub const href = bridge.accessor(Link.getHref, Link.setHref, .{}); pub const href = bridge.accessor(Link.getHref, Link.setHref, .{});
pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true });
fn _getRelList(self: *Link, page: *Page) !?*@import("../../collections.zig").DOMTokenList {
const element = self.asElement();
// relList is only valid for HTML <link> elements, not SVG or MathML
if (element._namespace != .html) {
return null;
}
return element.getRelList(page);
}
}; };
const testing = @import("../../../../testing.zig"); const testing = @import("../../../../testing.zig");

View File

@@ -219,7 +219,7 @@ pub fn getForm(self: *Select, page: *Page) ?*Form {
// If form attribute exists, ONLY use that (even if it references nothing) // If form attribute exists, ONLY use that (even if it references nothing)
if (element.getAttributeSafe("form")) |form_id| { if (element.getAttributeSafe("form")) |form_id| {
if (page.document.getElementById(form_id, page)) |form_element| { if (page.document.getElementById(form_id)) |form_element| {
return form_element.is(Form); return form_element.is(Form);
} }
// form attribute present but invalid - no form owner // form attribute present but invalid - no form owner

View File

@@ -90,7 +90,7 @@ pub fn getForm(self: *TextArea, page: *Page) ?*Form {
// If form attribute exists, ONLY use that (even if it references nothing) // If form attribute exists, ONLY use that (even if it references nothing)
if (element.getAttributeSafe("form")) |form_id| { if (element.getAttributeSafe("form")) |form_id| {
if (page.document.getElementById(form_id, page)) |form_element| { if (page.document.getElementById(form_id)) |form_element| {
return form_element.is(Form); return form_element.is(Form);
} }
// form attribute present but invalid - no form owner // form attribute present but invalid - no form owner

View File

@@ -72,7 +72,6 @@ dependencies = [
"tikv-jemalloc-ctl", "tikv-jemalloc-ctl",
"tikv-jemallocator", "tikv-jemallocator",
"typed-arena", "typed-arena",
"xml5ever",
] ]
[[package]] [[package]]
@@ -477,13 +476,3 @@ name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "xml5ever"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee3f1e41afb31a75aef076563b0ad3ecc24f5bd9d12a72b132222664eb76b494"
dependencies = [
"log",
"markup5ever",
]

View File

@@ -14,7 +14,6 @@ string_cache = "0.9.0"
typed-arena = "2.0.2" typed-arena = "2.0.2"
tikv-jemallocator = {version = "0.6.0", features = ["stats"]} tikv-jemallocator = {version = "0.6.0", features = ["stats"]}
tikv-jemalloc-ctl = {version = "0.6.0", features = ["stats"]} tikv-jemalloc-ctl = {version = "0.6.0", features = ["stats"]}
xml5ever = "0.35.0"
[profile.release] [profile.release]
lto = true lto = true

View File

@@ -16,20 +16,20 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
mod sink;
mod types; mod types;
mod sink;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
#[global_allocator] #[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
use types::*;
use std::cell::Cell; use std::cell::Cell;
use std::os::raw::{c_uchar, c_void}; use std::os::raw::{c_uchar, c_void};
use types::*;
use html5ever::{parse_document, parse_fragment, QualName, LocalName, ns, ParseOpts, Parser};
use html5ever::tendril::{TendrilSink, StrTendril};
use html5ever::interface::tree_builder::QuirksMode; use html5ever::interface::tree_builder::QuirksMode;
use html5ever::tendril::{StrTendril, TendrilSink};
use html5ever::{ns, parse_document, parse_fragment, LocalName, ParseOpts, Parser, QualName};
#[no_mangle] #[no_mangle]
pub extern "C" fn html5ever_parse_document( pub extern "C" fn html5ever_parse_document(
@@ -135,14 +135,13 @@ pub extern "C" fn html5ever_parse_fragment(
let bytes = unsafe { std::slice::from_raw_parts(html, len) }; let bytes = unsafe { std::slice::from_raw_parts(html, len) };
parse_fragment( parse_fragment(
sink, sink, Default::default(),
Default::default(),
QualName::new(None, ns!(html), LocalName::from("body")), QualName::new(None, ns!(html), LocalName::from("body")),
vec![], // attributes vec![], // attributes
false, // context_element_allows_scripting false, // context_element_allows_scripting
) )
.from_utf8() .from_utf8()
.one(bytes); .one(bytes);
} }
#[no_mangle] #[no_mangle]
@@ -183,15 +182,15 @@ pub struct Memory {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
#[no_mangle] #[no_mangle]
pub extern "C" fn html5ever_get_memory_usage() -> Memory { pub extern "C" fn html5ever_get_memory_usage() -> Memory {
use tikv_jemalloc_ctl::{epoch, stats}; use tikv_jemalloc_ctl::{stats, epoch};
// many statistics are cached and only updated when the epoch is advanced. // many statistics are cached and only updated when the epoch is advanced.
epoch::advance().unwrap(); epoch::advance().unwrap();
return Memory { return Memory{
resident: stats::resident::read().unwrap(), resident: stats::resident::read().unwrap(),
allocated: stats::allocated::read().unwrap(), allocated: stats::allocated::read().unwrap(),
}; }
} }
// Streaming parser API // Streaming parser API
@@ -226,8 +225,9 @@ pub extern "C" fn html5ever_streaming_parser_create(
// SAFETY: We're creating a self-referential structure here. // SAFETY: We're creating a self-referential structure here.
// The arena is stored in the StreamingParser and lives as long as the parser. // The arena is stored in the StreamingParser and lives as long as the parser.
// The sink contains a reference to the arena that's valid for the parser's lifetime. // The sink contains a reference to the arena that's valid for the parser's lifetime.
let arena_ref: &'static typed_arena::Arena<sink::ElementData> = let arena_ref: &'static typed_arena::Arena<sink::ElementData> = unsafe {
unsafe { std::mem::transmute(arena.as_ref()) }; std::mem::transmute(arena.as_ref())
};
let sink = sink::Sink { let sink = sink::Sink {
ctx: ctx, ctx: ctx,
@@ -281,8 +281,7 @@ pub extern "C" fn html5ever_streaming_parser_feed(
// Feed the chunk to the parser // Feed the chunk to the parser
// The Parser implements TendrilSink, so we can call process() on it // The Parser implements TendrilSink, so we can call process() on it
let parser = streaming_parser let parser = streaming_parser.parser
.parser
.downcast_mut::<Parser<sink::Sink>>() .downcast_mut::<Parser<sink::Sink>>()
.expect("Invalid parser type"); .expect("Invalid parser type");
@@ -305,8 +304,7 @@ pub extern "C" fn html5ever_streaming_parser_finish(parser_ptr: *mut c_void) {
let streaming_parser = unsafe { Box::from_raw(parser_ptr as *mut StreamingParser) }; let streaming_parser = unsafe { Box::from_raw(parser_ptr as *mut StreamingParser) };
// Extract and finish the parser // Extract and finish the parser
let parser = streaming_parser let parser = streaming_parser.parser
.parser
.downcast::<Parser<sink::Sink>>() .downcast::<Parser<sink::Sink>>()
.expect("Invalid parser type"); .expect("Invalid parser type");
@@ -328,57 +326,3 @@ pub extern "C" fn html5ever_streaming_parser_destroy(parser_ptr: *mut c_void) {
let _ = Box::from_raw(parser_ptr as *mut StreamingParser); let _ = Box::from_raw(parser_ptr as *mut StreamingParser);
} }
} }
#[no_mangle]
pub extern "C" fn xml5ever_parse_document(
xml: *mut c_uchar,
len: usize,
document: Ref,
ctx: Ref,
create_element_callback: CreateElementCallback,
get_data_callback: GetDataCallback,
append_callback: AppendCallback,
parse_error_callback: ParseErrorCallback,
pop_callback: PopCallback,
create_comment_callback: CreateCommentCallback,
create_processing_instruction: CreateProcessingInstruction,
append_doctype_to_document: AppendDoctypeToDocumentCallback,
add_attrs_if_missing_callback: AddAttrsIfMissingCallback,
get_template_contents_callback: GetTemplateContentsCallback,
remove_from_parent_callback: RemoveFromParentCallback,
reparent_children_callback: ReparentChildrenCallback,
append_before_sibling_callback: AppendBeforeSiblingCallback,
append_based_on_parent_node_callback: AppendBasedOnParentNodeCallback,
) -> () {
if xml.is_null() || len == 0 {
return ();
}
let arena = typed_arena::Arena::new();
let sink = sink::Sink {
ctx: ctx,
arena: &arena,
document: document,
quirks_mode: Cell::new(QuirksMode::NoQuirks),
pop_callback: pop_callback,
append_callback: append_callback,
get_data_callback: get_data_callback,
parse_error_callback: parse_error_callback,
create_element_callback: create_element_callback,
create_comment_callback: create_comment_callback,
create_processing_instruction: create_processing_instruction,
append_doctype_to_document: append_doctype_to_document,
add_attrs_if_missing_callback: add_attrs_if_missing_callback,
get_template_contents_callback: get_template_contents_callback,
remove_from_parent_callback: remove_from_parent_callback,
reparent_children_callback: reparent_children_callback,
append_before_sibling_callback: append_before_sibling_callback,
append_based_on_parent_node_callback: append_based_on_parent_node_callback,
};
let bytes = unsafe { std::slice::from_raw_parts(xml, len) };
xml5ever::driver::parse_document(sink, xml5ever::driver::XmlParseOpts::default())
.from_utf8()
.one(bytes);
}