mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 15:40:04 +00:00
Compare commits
1 Commits
v0.2.0
...
zigdom-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6e8aff2c9 |
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.2.2'
|
||||
default: 'v0.1.37'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
|
||||
32
.github/workflows/build.yml
vendored
32
.github/workflows/build.yml
vendored
@@ -5,12 +5,8 @@ env:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
|
||||
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
||||
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
schedule:
|
||||
- cron: "2 2 * * *"
|
||||
|
||||
@@ -42,11 +38,8 @@ jobs:
|
||||
arch: ${{env.ARCH}}
|
||||
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
|
||||
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
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
@@ -61,7 +54,7 @@ jobs:
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: ${{ env.RELEASE }}
|
||||
tag: nightly
|
||||
|
||||
build-linux-aarch64:
|
||||
env:
|
||||
@@ -84,11 +77,8 @@ jobs:
|
||||
arch: ${{env.ARCH}}
|
||||
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
|
||||
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
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
@@ -103,7 +93,7 @@ jobs:
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: ${{ env.RELEASE }}
|
||||
tag: nightly
|
||||
|
||||
build-macos-aarch64:
|
||||
env:
|
||||
@@ -128,11 +118,8 @@ jobs:
|
||||
arch: ${{env.ARCH}}
|
||||
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
|
||||
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
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
@@ -147,7 +134,7 @@ jobs:
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: ${{ env.RELEASE }}
|
||||
tag: nightly
|
||||
|
||||
build-macos-x86_64:
|
||||
env:
|
||||
@@ -170,11 +157,8 @@ jobs:
|
||||
arch: ${{env.ARCH}}
|
||||
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
|
||||
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
|
||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
@@ -189,4 +173,4 @@ jobs:
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||
tag: ${{ env.RELEASE }}
|
||||
tag: nightly
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -3,7 +3,7 @@ FROM debian:stable-slim
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG V8=14.0.365.4
|
||||
ARG ZIG_V8=v0.2.2
|
||||
ARG ZIG_V8=v0.1.37
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
@@ -48,16 +48,8 @@ RUN case $TARGETPLATFORM in \
|
||||
mkdir -p v8/ && \
|
||||
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
|
||||
RUN zig build -Doptimize=ReleaseFast \
|
||||
-Dsnapshot_path=../../snapshot.bin \
|
||||
-Dprebuilt_v8_path=v8/libc_v8.a \
|
||||
-Dgit_commit=$(git rev-parse --short HEAD)
|
||||
RUN zig build -Doptimize=ReleaseFast -Dprebuilt_v8_path=v8/libc_v8.a -Dgit_commit=$(git rev-parse --short HEAD)
|
||||
|
||||
FROM debian:stable-slim
|
||||
|
||||
|
||||
14
Makefile
14
Makefile
@@ -47,18 +47,12 @@ help:
|
||||
|
||||
# $(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-v8-snapshot:
|
||||
@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
|
||||
## Build in release-safe mode
|
||||
build:
|
||||
@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"
|
||||
|
||||
## Build in debug mode
|
||||
|
||||
17
README.md
17
README.md
@@ -211,23 +211,6 @@ env.
|
||||
|
||||
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
|
||||
|
||||
### Unit Tests
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/d6b5f89cfc7feece29359e8c848bb916e8ecfab6.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH6_0gBABrJc5cL6-P2wGvvweTTCgWdpmClr9r-C-s",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/0d64a3d5b36ac94067df3e13fddbf715caa6f391.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH65sfBAC8o3q41YxhOms5uY2fvMzBrsgN8IeCXZgE",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
.@"boringssl-zig" = .{
|
||||
|
||||
@@ -40,8 +40,7 @@ arena: Allocator,
|
||||
listener_pool: std.heap.MemoryPool(Listener),
|
||||
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
|
||||
lookup: std.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList),
|
||||
dispatch_depth: usize,
|
||||
deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),
|
||||
dispatch_depth: u32 = 0,
|
||||
|
||||
pub fn init(page: *Page) EventManager {
|
||||
return .{
|
||||
@@ -51,7 +50,6 @@ pub fn init(page: *Page) EventManager {
|
||||
.list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena),
|
||||
.listener_pool = std.heap.MemoryPool(Listener).init(page.arena),
|
||||
.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 {
|
||||
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;
|
||||
if (findListener(list, typ, callback, use_capture)) |listener| {
|
||||
self.removeListener(list, listener);
|
||||
@@ -185,7 +186,7 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
||||
|
||||
if (function_) |func| {
|
||||
event._current_target = target;
|
||||
if (func.callWithThis(void, target, .{event})) {
|
||||
if (func.call(void, .{event})) {
|
||||
was_dispatched = true;
|
||||
} else |err| {
|
||||
// a non-JS error
|
||||
@@ -298,53 +299,38 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
const page = self.page;
|
||||
const typ = event._type_string;
|
||||
|
||||
// Track dispatch depth for deferred removal
|
||||
// Track that we're dispatching to prevent immediate removal
|
||||
self.dispatch_depth += 1;
|
||||
defer {
|
||||
const dispatch_depth = self.dispatch_depth;
|
||||
// Only destroy deferred listeners when we exit the outermost dispatch
|
||||
if (dispatch_depth == 1) {
|
||||
for (self.deferred_removals.items) |removal| {
|
||||
removal.list.remove(&removal.listener.node);
|
||||
self.listener_pool.destroy(removal.listener);
|
||||
}
|
||||
self.deferred_removals.clearRetainingCapacity();
|
||||
} else {
|
||||
self.dispatch_depth = dispatch_depth - 1;
|
||||
}
|
||||
self.dispatch_depth -= 1;
|
||||
// Clean up any marked listeners in this target's list after this phase
|
||||
// We do this regardless of depth to handle cross-target removals correctly
|
||||
self.cleanupMarkedListeners(list);
|
||||
}
|
||||
|
||||
// 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 is_done = false;
|
||||
while (node) |n| {
|
||||
if (is_done) {
|
||||
break;
|
||||
}
|
||||
|
||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||
is_done = (listener == last_listener);
|
||||
// do this now, in case we need to remove n (once: true or aborted signal)
|
||||
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)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Can be null when dispatching to the target itself
|
||||
if (comptime capture_only) |capture| {
|
||||
if (listener.capture != capture) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip removed listeners
|
||||
if (listener.removed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the listener has an aborted signal, remove it and skip
|
||||
if (listener.signal) |signal| {
|
||||
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;
|
||||
event._current_target = current_target;
|
||||
|
||||
@@ -368,7 +349,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
}
|
||||
|
||||
switch (listener.function) {
|
||||
.value => |value| try value.callWithThis(void, current_target, .{event}),
|
||||
.value => |value| try value.call(void, .{event}),
|
||||
.string => |string| {
|
||||
const str = try page.call_arena.dupeZ(u8, string.str());
|
||||
try self.page.js.eval(str, null);
|
||||
@@ -385,6 +366,10 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
event._target = original_target;
|
||||
}
|
||||
|
||||
if (listener.once) {
|
||||
self.removeListener(list, listener);
|
||||
}
|
||||
|
||||
if (event._stop_immediate_propagation) {
|
||||
return;
|
||||
}
|
||||
@@ -397,17 +382,29 @@ fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target:
|
||||
}
|
||||
|
||||
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) {
|
||||
listener.removed = true;
|
||||
self.deferred_removals.append(self.arena, .{ .list = list, .listener = listener }) catch unreachable;
|
||||
// We're in the middle of dispatching, just mark for removal
|
||||
// This prevents invalidating the linked list during iteration
|
||||
listener.marked_for_removal = true;
|
||||
} else {
|
||||
// Outside dispatch, remove immediately
|
||||
// Safe to remove immediately
|
||||
list.remove(&listener.node);
|
||||
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 {
|
||||
var node = list.first;
|
||||
while (node) |n| {
|
||||
@@ -439,7 +436,7 @@ const Listener = struct {
|
||||
function: Function,
|
||||
signal: ?*@import("webapi/AbortSignal.zig") = null,
|
||||
node: std.DoublyLinkedList.Node,
|
||||
removed: bool = false,
|
||||
marked_for_removal: bool = false,
|
||||
};
|
||||
|
||||
const Function = union(enum) {
|
||||
|
||||
@@ -168,18 +168,6 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
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
|
||||
pub fn event(self: *Factory, typ: []const u8, child: anytype) !*@TypeOf(child) {
|
||||
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
|
||||
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);
|
||||
|
||||
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
|
||||
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.setLeaf(2, child);
|
||||
|
||||
|
||||
@@ -93,9 +93,7 @@ _attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attri
|
||||
_element_styles: Element.StyleLookup = .{},
|
||||
_element_datasets: Element.DatasetLookup = .{},
|
||||
_element_class_lists: Element.ClassListLookup = .{},
|
||||
_element_rel_lists: Element.RelListLookup = .{},
|
||||
_element_shadow_roots: Element.ShadowRootLookup = .{},
|
||||
_node_owner_documents: Node.OwnerDocumentLookup = .{},
|
||||
_element_assigned_slots: Element.AssignedSlotLookup = .{},
|
||||
|
||||
_script_manager: ScriptManager,
|
||||
@@ -265,9 +263,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
||||
self._element_styles = .{};
|
||||
self._element_datasets = .{};
|
||||
self._element_class_lists = .{};
|
||||
self._element_rel_lists = .{};
|
||||
self._element_shadow_roots = .{};
|
||||
self._node_owner_documents = .{};
|
||||
self._element_assigned_slots = .{};
|
||||
self._notified_network_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) ElementIdMaps {
|
||||
fn getElementIdMap(page: *Page, node: *Node) *std.StringHashMapUnmanaged(*Element) {
|
||||
// Walk up the tree checking for ShadowRoot and tracking the root
|
||||
var current = node;
|
||||
while (true) {
|
||||
if (current.is(ShadowRoot)) |shadow_root| {
|
||||
return .{
|
||||
.lookup = &shadow_root._elements_by_id,
|
||||
.removed_ids = &shadow_root._removed_ids,
|
||||
};
|
||||
return &shadow_root._elements_by_id;
|
||||
}
|
||||
|
||||
const parent = current._parent orelse {
|
||||
if (current._type == .document) {
|
||||
return .{
|
||||
.lookup = ¤t._type.document._elements_by_id,
|
||||
.removed_ids = ¤t._type.document._removed_ids,
|
||||
};
|
||||
return ¤t._type.document._elements_by_id;
|
||||
}
|
||||
// Detached nodes should not have IDs registered
|
||||
std.debug.assert(false);
|
||||
return .{
|
||||
.lookup = &page.document._elements_by_id,
|
||||
.removed_ids = &page.document._removed_ids,
|
||||
};
|
||||
return &page.document._elements_by_id;
|
||||
};
|
||||
|
||||
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 {
|
||||
var id_maps = self.getElementIdMap(parent);
|
||||
const gop = try id_maps.lookup.getOrPut(self.arena, id);
|
||||
var id_map = self.getElementIdMap(parent);
|
||||
const gop = try id_map.getOrPut(self.arena, id);
|
||||
if (!gop.found_existing) {
|
||||
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 {
|
||||
const node = element.asNode();
|
||||
self.removeElementIdWithMaps(self.getElementIdMap(node), 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 {};
|
||||
}
|
||||
var id_map = self.getElementIdMap(element.asNode());
|
||||
_ = id_map.remove(id);
|
||||
}
|
||||
|
||||
pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Element {
|
||||
if (node.isConnected() or node.isInShadowTree()) {
|
||||
const lookup = self.getElementIdMap(node).lookup;
|
||||
return lookup.get(id);
|
||||
const id_map = self.getElementIdMap(node);
|
||||
return id_map.get(id);
|
||||
}
|
||||
var tw = @import("webapi/TreeWalker.zig").Full.Elements.init(node, .{});
|
||||
while (tw.next()) |el| {
|
||||
@@ -1315,26 +1287,6 @@ pub fn nodeComplete(self: *Page, node: *Node) !void {
|
||||
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 {
|
||||
const namespace: Element.Namespace = blk: {
|
||||
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
|
||||
const was_connected = child.isConnected();
|
||||
// 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._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, .{});
|
||||
while (tw.next()) |el| {
|
||||
if (el.getAttributeSafe("id")) |id| {
|
||||
self.removeElementIdWithMaps(id_maps.?, id);
|
||||
_ = id_map.?.remove(id);
|
||||
}
|
||||
|
||||
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();
|
||||
const dest_connected = parent.isConnected();
|
||||
const dest_connected = target.isConnected();
|
||||
|
||||
var it = fragment.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
@@ -2228,7 +2180,7 @@ pub fn insertAllChildrenBefore(self: *Page, fragment: *Node, parent: *Node, ref_
|
||||
const child_was_connected = child.isConnected();
|
||||
self.removeNode(fragment, child, .{ .will_be_reconnected = dest_connected });
|
||||
try self.insertNodeRelative(
|
||||
parent,
|
||||
target,
|
||||
child,
|
||||
.{ .before = ref_node },
|
||||
.{ .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) {
|
||||
append,
|
||||
after: *Node,
|
||||
|
||||
@@ -239,17 +239,8 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
};
|
||||
|
||||
const is_blocking = script.mode == .normal;
|
||||
if (is_blocking == false) {
|
||||
self.scriptList(script).append(&script.node);
|
||||
}
|
||||
|
||||
if (remote_url) |url| {
|
||||
errdefer {
|
||||
if (is_blocking == false) {
|
||||
self.scriptList(script).remove(&script.node);
|
||||
}
|
||||
script.deinit(true);
|
||||
}
|
||||
errdefer script.deinit(true);
|
||||
|
||||
var headers = try self.client.newHeaders();
|
||||
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) {
|
||||
const list = self.scriptList(script);
|
||||
list.append(&script.node);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -644,10 +644,8 @@ fn consumeNumeric(self: *Tokenizer) Token {
|
||||
fn consumeUnquotedUrl(self: *Tokenizer) ?Token {
|
||||
// TODO: true url parser
|
||||
if (self.nextByte()) |it| {
|
||||
return self.consumeString(it == '\'');
|
||||
self.consumeString(it == '\'');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn consumeIdentLike(self: *Tokenizer) Token {
|
||||
|
||||
@@ -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 {
|
||||
if (!info.isConstructCall()) {
|
||||
self.handleError(T, @TypeOf(func), error.InvalidArgument, info, opts);
|
||||
return;
|
||||
}
|
||||
self._constructor(func, info) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
};
|
||||
|
||||
@@ -203,6 +203,27 @@ fn trackCallback(self: *Context, pf: PersistentFunction) !void {
|
||||
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 ==
|
||||
pub fn eval(self: *Context, src: []const u8, name: ?[]const u8) !void {
|
||||
_ = 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_obj = js_arr.castTo(v8.Object);
|
||||
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) {
|
||||
return error.FailedToCreateArray;
|
||||
}
|
||||
@@ -556,7 +577,7 @@ pub fn zigValueToJs(self: *Context, value: anytype, comptime opts: Caller.CallOp
|
||||
},
|
||||
.optional => {
|
||||
if (value) |v| {
|
||||
return self.zigValueToJs(v, opts);
|
||||
return self.zigValueToJs(v, .{});
|
||||
}
|
||||
// would be handled by simpleZigValueToJs
|
||||
unreachable;
|
||||
|
||||
@@ -74,23 +74,26 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
|
||||
|
||||
const env = self.env;
|
||||
const isolate = env.isolate;
|
||||
const arena = self.context_arena.allocator();
|
||||
|
||||
var v8_context: v8.Context = blk: {
|
||||
var temp_scope: v8.HandleScope = undefined;
|
||||
v8.HandleScope.init(&temp_scope, isolate);
|
||||
defer temp_scope.deinit();
|
||||
|
||||
// Creates a global template that inherits from Window.
|
||||
const global_template = @import("Snapshot.zig").createGlobalTemplate(isolate, env.templates);
|
||||
if (comptime IS_DEBUG) {
|
||||
// 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{
|
||||
.getter = unknownPropertyCallback,
|
||||
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
|
||||
}, 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();
|
||||
break :blk v8_context;
|
||||
};
|
||||
@@ -121,7 +124,7 @@ pub fn createContext(self: *ExecutionWorld, page: *Page, enter: bool) !*Context
|
||||
.handle_scope = handle_scope,
|
||||
.script_manager = &page._script_manager,
|
||||
.call_arena = page.call_arena,
|
||||
.arena = arena,
|
||||
.arena = self.context_arena.allocator(),
|
||||
};
|
||||
|
||||
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 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
|
||||
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(.{
|
||||
.{ "process", {} },
|
||||
@@ -182,27 +185,13 @@ pub fn unknownPropertyCallback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C
|
||||
.{ "CLOSURE_FLAGS", {} },
|
||||
});
|
||||
|
||||
if (maybe_property) |prop| {
|
||||
if (!ignored.has(prop)) {
|
||||
const page = context.page;
|
||||
const document = page.document;
|
||||
|
||||
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", .{
|
||||
if (!ignored.has(property)) {
|
||||
log.debug(.unknown_prop, "unkown global property", .{
|
||||
.info = "but the property can exist in pure JS",
|
||||
.stack = context.stackTrace() catch "???",
|
||||
.property = prop,
|
||||
.property = property,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return v8.Intercepted.No;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
const context = self.context;
|
||||
|
||||
// When we're calling a function from within JavaScript itself, this isn't
|
||||
// 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 js_this = try context.valueToExistingObject(this);
|
||||
|
||||
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ pub fn set(self: Object, key: []const u8, value: anytype, opts: SetOpts) error{
|
||||
const context = self.context;
|
||||
|
||||
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;
|
||||
if (!res) {
|
||||
|
||||
@@ -113,17 +113,6 @@ fn isValid(self: Snapshot) bool {
|
||||
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 {
|
||||
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
|
||||
// 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);
|
||||
context.enter();
|
||||
@@ -411,7 +407,7 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
|
||||
},
|
||||
bridge.Function => {
|
||||
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) {
|
||||
template.set(js_name, function_template, v8.PropertyAttribute.None);
|
||||
} else {
|
||||
@@ -460,12 +456,6 @@ fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.FunctionT
|
||||
instance_template.markAsUndetectable();
|
||||
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 {
|
||||
|
||||
@@ -41,14 +41,6 @@ pub fn isArray(self: Value) bool {
|
||||
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 {
|
||||
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() };
|
||||
}
|
||||
|
||||
pub fn toZig(self: Value, comptime T: type) !T {
|
||||
return self.context.jsValueToZig(T, self.js_val);
|
||||
}
|
||||
|
||||
pub fn toObject(self: Value) js.Object {
|
||||
return .{
|
||||
.context = self.context,
|
||||
|
||||
@@ -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 {
|
||||
h5e.html5ever_parse_fragment(
|
||||
html.ptr,
|
||||
|
||||
@@ -171,24 +171,3 @@ pub const NodeOrText = extern struct {
|
||||
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;
|
||||
|
||||
@@ -107,6 +107,19 @@
|
||||
}
|
||||
</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>
|
||||
{
|
||||
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></body></html>', new DOMParser().parseFromString('<html></html>', "text/html").documentElement.outerHTML);
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -37,7 +37,4 @@
|
||||
testing.expectEqual(null, c2.parentNode);
|
||||
assertChildren([c3, c4], d1)
|
||||
assertChildren([], d2)
|
||||
|
||||
testing.expectEqual(c3, d1.replaceChild(c3, c3));
|
||||
assertChildren([c3, c4], d1)
|
||||
</script>
|
||||
|
||||
@@ -376,447 +376,3 @@
|
||||
testing.expectEqual('Bold', fragment.childNodes[0].textContent);
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
@@ -69,19 +69,6 @@ pub fn getCollapsed(self: *const AbstractRange) bool {
|
||||
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 {
|
||||
return compareBoundaryPoints(
|
||||
self._start_container,
|
||||
@@ -97,7 +84,7 @@ const BoundaryComparison = enum {
|
||||
after,
|
||||
};
|
||||
|
||||
pub fn compareBoundaryPoints(
|
||||
fn compareBoundaryPoints(
|
||||
node_a: *Node,
|
||||
offset_a: u32,
|
||||
node_b: *Node,
|
||||
@@ -208,13 +195,6 @@ fn isAncestorOf(potential_ancestor: *Node, node: *Node) bool {
|
||||
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 bridge = js.Bridge(AbstractRange);
|
||||
|
||||
@@ -229,5 +209,4 @@ pub const JsApi = struct {
|
||||
pub const endContainer = bridge.accessor(AbstractRange.getEndContainer, null, .{});
|
||||
pub const endOffset = bridge.accessor(AbstractRange.getEndOffset, null, .{});
|
||||
pub const collapsed = bridge.accessor(AbstractRange.getCollapsed, null, .{});
|
||||
pub const commonAncestorContainer = bridge.accessor(AbstractRange.getCommonAncestorContainer, null, .{});
|
||||
};
|
||||
|
||||
@@ -147,7 +147,7 @@ pub fn format(self: *const CData, writer: *std.io.Writer) !void {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -120,6 +120,11 @@ pub const JsApi = struct {
|
||||
pub const createDocument = bridge.function(DOMImplementation.createDocument, .{});
|
||||
pub const createHTMLDocument = bridge.function(DOMImplementation.createHTMLDocument, .{});
|
||||
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");
|
||||
|
||||
@@ -19,13 +19,8 @@
|
||||
const std = @import("std");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const Parser = @import("../parser/Parser.zig");
|
||||
|
||||
const HTMLDocument = @import("HTMLDocument.zig");
|
||||
const XMLDocument = @import("XMLDocument.zig");
|
||||
const ProcessingInstruction = @import("../webapi/cdata/ProcessingInstruction.zig");
|
||||
|
||||
const DOMParser = @This();
|
||||
|
||||
@@ -33,27 +28,14 @@ pub fn init() DOMParser {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub const HTMLDocumentOrXMLDocument = union(enum) {
|
||||
html_document: *HTMLDocument,
|
||||
xml_document: *XMLDocument,
|
||||
};
|
||||
pub fn parseFromString(self: *const DOMParser, html: []const u8, mime_type: []const u8, page: *Page) !*HTMLDocument {
|
||||
_ = self;
|
||||
|
||||
pub fn parseFromString(
|
||||
_: *const DOMParser,
|
||||
html: []const u8,
|
||||
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);
|
||||
// For now, only support text/html
|
||||
if (!std.mem.eql(u8, mime_type, "text/html")) {
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
if (maybe_target_mime) |target_mime| switch (target_mime) {
|
||||
.@"text/html" => {
|
||||
// Create a new HTMLDocument
|
||||
const doc = try page._factory.document(HTMLDocument{
|
||||
._proto = undefined,
|
||||
@@ -65,6 +47,7 @@ pub fn parseFromString(
|
||||
}
|
||||
|
||||
// Parse HTML into the document
|
||||
const Parser = @import("../parser/Parser.zig");
|
||||
var parser = Parser.init(page.arena, doc.asNode(), page);
|
||||
parser.parse(normalized);
|
||||
|
||||
@@ -72,39 +55,7 @@ pub fn parseFromString(
|
||||
return pe.err;
|
||||
}
|
||||
|
||||
return .{ .html_document = 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;
|
||||
return doc;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -79,81 +79,25 @@ pub fn parentNode(self: *DOMTreeWalker) !?*Node {
|
||||
|
||||
pub fn firstChild(self: *DOMTreeWalker) !?*Node {
|
||||
var node = self._current.firstChild();
|
||||
|
||||
while (node) |n| {
|
||||
const filter_result = try self.acceptNode(n);
|
||||
|
||||
if (filter_result == NodeFilter.FILTER_ACCEPT) {
|
||||
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) {
|
||||
self._current = n;
|
||||
return n;
|
||||
}
|
||||
|
||||
if (filter_result == NodeFilter.FILTER_SKIP) {
|
||||
// Descend into children of this skipped node
|
||||
if (n.firstChild()) |child| {
|
||||
node = child;
|
||||
continue;
|
||||
node = self.nextSiblingOrNull(n);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
pub fn lastChild(self: *DOMTreeWalker) !?*Node {
|
||||
var node = self._current.lastChild();
|
||||
|
||||
while (node) |n| {
|
||||
const filter_result = try self.acceptNode(n);
|
||||
|
||||
if (filter_result == NodeFilter.FILTER_ACCEPT) {
|
||||
if (try self.acceptNode(n) == NodeFilter.FILTER_ACCEPT) {
|
||||
self._current = n;
|
||||
return n;
|
||||
}
|
||||
|
||||
if (filter_result == NodeFilter.FILTER_SKIP) {
|
||||
// Descend into children of this skipped node
|
||||
if (n.lastChild()) |child| {
|
||||
node = child;
|
||||
continue;
|
||||
node = self.previousSiblingOrNull(n);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -187,39 +131,15 @@ pub fn previousNode(self: *DOMTreeWalker) !?*Node {
|
||||
var sibling = self.previousSiblingOrNull(node);
|
||||
while (sibling) |sib| {
|
||||
node = sib;
|
||||
|
||||
// Check if this sibling is rejected before descending into it
|
||||
const sib_result = try self.acceptNode(node);
|
||||
if (sib_result == NodeFilter.FILTER_REJECT) {
|
||||
// Skip this sibling and its descendants entirely
|
||||
sibling = self.previousSiblingOrNull(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (self.isInSubtree(c)) {
|
||||
node = c;
|
||||
child = self.lastChildOrNull(node);
|
||||
} 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) {
|
||||
self._current = node;
|
||||
return node;
|
||||
|
||||
@@ -48,8 +48,6 @@ _location: ?*Location = null,
|
||||
_ready_state: ReadyState = .loading,
|
||||
_current_script: ?*Element.Html.Script = null,
|
||||
_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,
|
||||
_style_sheets: ?*StyleSheetList = null,
|
||||
_write_insertion_point: ?*Node = null,
|
||||
@@ -123,15 +121,10 @@ const CreateElementOptions = struct {
|
||||
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 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;
|
||||
if (options.is) |is_value| {
|
||||
try element.setAttribute("is", is_value, page);
|
||||
@@ -141,13 +134,8 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement
|
||||
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);
|
||||
|
||||
// Track owner document if it's not the main document
|
||||
if (self != page.document) {
|
||||
try page.setNodeOwnerDocument(node, self);
|
||||
}
|
||||
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 {
|
||||
if (id.len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
pub fn getElementById(self: *const Document, id_: ?[]const u8) ?*Element {
|
||||
const id = id_ orelse return null;
|
||||
return self._elements_by_id.get(id);
|
||||
}
|
||||
|
||||
const GetElementsByTagNameResult = union(enum) {
|
||||
@@ -287,53 +252,28 @@ pub fn getImplementation(_: *const Document) DOMImplementation {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn createDocumentFragment(self: *Document, page: *Page) !*Node.DocumentFragment {
|
||||
const frag = try 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 createDocumentFragment(_: *const Document, page: *Page) !*Node.DocumentFragment {
|
||||
return Node.DocumentFragment.init(page);
|
||||
}
|
||||
|
||||
pub fn createComment(self: *Document, data: []const u8, page: *Page) !*Node {
|
||||
const node = try 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 createComment(_: *const Document, data: []const u8, page: *Page) !*Node {
|
||||
return page.createComment(data);
|
||||
}
|
||||
|
||||
pub fn createTextNode(self: *Document, data: []const u8, page: *Page) !*Node {
|
||||
const node = try 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 createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node {
|
||||
return page.createTextNode(data);
|
||||
}
|
||||
|
||||
pub fn createCDATASection(self: *Document, data: []const u8, page: *Page) !*Node {
|
||||
const node = switch (self._type) {
|
||||
pub fn createCDATASection(self: *const Document, data: []const u8, page: *Page) !*Node {
|
||||
switch (self._type) {
|
||||
.html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument
|
||||
.xml => try page.createCDATASection(data),
|
||||
.generic => try page.createCDATASection(data),
|
||||
};
|
||||
// Track owner document if it's not the main document
|
||||
if (self != page.document) {
|
||||
try page.setNodeOwnerDocument(node, self);
|
||||
.xml => return page.createCDATASection(data),
|
||||
.generic => return page.createCDATASection(data),
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, page: *Page) !*Node {
|
||||
const node = try 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;
|
||||
pub fn createProcessingInstruction(_: *const Document, target: []const u8, data: []const u8, page: *Page) !*Node {
|
||||
return page.createProcessingInstruction(target, data);
|
||||
}
|
||||
|
||||
const Range = @import("Range.zig");
|
||||
@@ -361,26 +301,14 @@ pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@i
|
||||
return error.NotSupported;
|
||||
}
|
||||
|
||||
pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker {
|
||||
return DOMTreeWalker.init(root, try whatToShow(what_to_show), filter, page);
|
||||
pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker {
|
||||
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 {
|
||||
return DOMNodeIterator.init(root, try whatToShow(what_to_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 createNodeIterator(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator {
|
||||
const show = what_to_show orelse NodeFilter.SHOW_ALL;
|
||||
return DOMNodeIterator.init(root, show, filter, page);
|
||||
}
|
||||
|
||||
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 createTreeWalker = bridge.function(Document.createTreeWalker, .{});
|
||||
pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{});
|
||||
pub const getElementById = bridge.function(_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 getElementById = bridge.function(Document.getElementById, .{});
|
||||
pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true });
|
||||
pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true });
|
||||
pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{});
|
||||
|
||||
@@ -71,10 +71,8 @@ pub fn className(_: *const DocumentFragment) []const u8 {
|
||||
return "[object DocumentFragment]";
|
||||
}
|
||||
|
||||
pub fn getElementById(self: *DocumentFragment, id: []const u8) ?*Element {
|
||||
if (id.len == 0) {
|
||||
return null;
|
||||
}
|
||||
pub fn getElementById(self: *DocumentFragment, id_: ?[]const u8) ?*Element {
|
||||
const id = id_ orelse return null;
|
||||
|
||||
var tw = @import("TreeWalker.zig").Full.Elements.init(self.asNode(), .{});
|
||||
while (tw.next()) |el| {
|
||||
@@ -158,12 +156,6 @@ pub fn replaceChildren(self: *DocumentFragment, nodes: []const Node.NodeOrText,
|
||||
const parent_is_connected = parent.isConnected();
|
||||
for (nodes) |node_or_text| {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -241,18 +233,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const constructor = bridge.constructor(DocumentFragment.init, .{});
|
||||
|
||||
pub const getElementById = bridge.function(_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 getElementById = bridge.function(DocumentFragment.getElementById, .{});
|
||||
pub const querySelector = bridge.function(DocumentFragment.querySelector, .{ .dom_exception = true });
|
||||
pub const querySelectorAll = bridge.function(DocumentFragment.querySelectorAll, .{ .dom_exception = true });
|
||||
pub const children = bridge.accessor(DocumentFragment.getChildren, null, .{});
|
||||
|
||||
@@ -70,4 +70,9 @@ pub const JsApi = struct {
|
||||
pub const name = bridge.accessor(DocumentType.getName, null, .{});
|
||||
pub const publicId = bridge.accessor(DocumentType.getPublicId, 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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -44,7 +44,6 @@ const Element = @This();
|
||||
pub const DatasetLookup = std.AutoHashMapUnmanaged(*Element, *DOMStringMap);
|
||||
pub const StyleLookup = std.AutoHashMapUnmanaged(*Element, *CSSStyleProperties);
|
||||
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 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);
|
||||
}
|
||||
|
||||
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 {
|
||||
const dump = @import("../dump.zig");
|
||||
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.*;
|
||||
}
|
||||
|
||||
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 {
|
||||
const gop = try page._element_datasets.getOrPut(page.arena, self);
|
||||
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 it = std.mem.tokenizeAny(u8, class_name, &std.ascii.whitespace);
|
||||
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);
|
||||
@@ -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);
|
||||
|
||||
// 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) {
|
||||
var child_it = self.asNode().childrenIterator();
|
||||
while (child_it.next()) |child| {
|
||||
@@ -1239,7 +1208,7 @@ pub const JsApi = struct {
|
||||
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 {
|
||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try self.getOuterHTML(&buf.writer, page);
|
||||
|
||||
@@ -40,7 +40,7 @@ _prevent_default: bool = false,
|
||||
_stop_propagation: bool = false,
|
||||
_stop_immediate_propagation: bool = false,
|
||||
_event_phase: EventPhase = .none,
|
||||
_time_stamp: u64,
|
||||
_time_stamp: u64 = 0,
|
||||
_needs_retargeting: bool = false,
|
||||
_isTrusted: bool = false,
|
||||
|
||||
@@ -105,14 +105,9 @@ pub fn initEvent(
|
||||
cancelable: ?bool,
|
||||
page: *Page,
|
||||
) !void {
|
||||
if (self._event_phase != .none) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._type_string = try String.init(page.arena, event_string, .{});
|
||||
self._bubbles = bubbles orelse false;
|
||||
self._cancelable = cancelable orelse false;
|
||||
self._stop_propagation = false;
|
||||
}
|
||||
|
||||
pub fn as(self: *Event, comptime T: type) *T {
|
||||
@@ -181,22 +176,6 @@ pub fn getDefaultPrevented(self: *const Event) bool {
|
||||
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 {
|
||||
return @intFromEnum(self._event_phase);
|
||||
}
|
||||
@@ -393,7 +372,6 @@ pub const JsApi = struct {
|
||||
pub const cancelable = bridge.accessor(Event.getCancelable, null, .{});
|
||||
pub const composed = bridge.accessor(Event.getComposed, 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 eventPhase = bridge.accessor(Event.getEventPhase, 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 composedPath = bridge.function(Event.composedPath, .{});
|
||||
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
|
||||
pub const NONE = bridge.property(@intFromEnum(EventPhase.none));
|
||||
|
||||
@@ -167,6 +167,7 @@ pub const JsApi = struct {
|
||||
pub const dispatchEvent = bridge.function(EventTarget.dispatchEvent, .{});
|
||||
pub const addEventListener = bridge.function(EventTarget.addEventListener, .{});
|
||||
pub const removeEventListener = bridge.function(EventTarget.removeEventListener, .{});
|
||||
pub const toString = bridge.function(EventTarget.toString, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -74,76 +74,30 @@ pub fn getBody(self: *HTMLDocument) ?*Element.Html.Body {
|
||||
}
|
||||
|
||||
pub fn getTitle(self: *HTMLDocument, page: *Page) ![]const u8 {
|
||||
// Search the entire document for the first <title> element
|
||||
const root = self._proto.getDocumentElement() orelse return "";
|
||||
const title_element = blk: {
|
||||
var walker = @import("TreeWalker.zig").Full.init(root.asNode(), .{});
|
||||
while (walker.next()) |node| {
|
||||
const head = self.getHead() orelse return "";
|
||||
var it = head.asNode().childrenIterator();
|
||||
while (it.next()) |node| {
|
||||
if (node.is(Element.Html.Title)) |title| {
|
||||
break :blk title;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
try title.asElement().getInnerText(&buf.writer);
|
||||
return buf.written();
|
||||
}
|
||||
}
|
||||
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 result.items;
|
||||
}
|
||||
|
||||
pub fn setTitle(self: *HTMLDocument, title: []const u8, page: *Page) !void {
|
||||
const head = self.getHead() orelse return;
|
||||
|
||||
// Find existing title element in head
|
||||
var it = head.asNode().childrenIterator();
|
||||
while (it.next()) |node| {
|
||||
if (node.is(Element.Html.Title)) |title_element| {
|
||||
// Replace children, but don't create text node for empty string
|
||||
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_element = title_node.as(Element);
|
||||
|
||||
// Only add text if non-empty
|
||||
if (title.len > 0) {
|
||||
try title_element.replaceChildren(&.{.{ .text = title }}, page);
|
||||
}
|
||||
|
||||
_ = try head.asNode().appendChild(title_node, page);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
|
||||
}
|
||||
|
||||
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();
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -46,9 +46,6 @@ _parent: ?*Node = null,
|
||||
_children: ?*Children = null,
|
||||
_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) {
|
||||
cdata: *CData,
|
||||
element: *Element,
|
||||
@@ -204,10 +201,6 @@ fn validateNodeInsertion(parent: *Node, node: *Node) !void {
|
||||
if (node.contains(parent)) {
|
||||
return error.HierarchyError;
|
||||
}
|
||||
|
||||
if (node._type == .attribute) {
|
||||
return error.HierarchyError;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// disconnect then reconnect)
|
||||
const child_connected = child.isConnected();
|
||||
|
||||
// Check if we're adopting the node to a different document
|
||||
const child_owner = child.ownerDocument(page);
|
||||
const parent_owner = self.ownerDocument(page) orelse self.as(Document);
|
||||
const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner;
|
||||
const child_root = child.getRootNode(null);
|
||||
const parent_root = self.getRootNode(null);
|
||||
const adopting_to_new_document = child_connected and child_root != parent_root;
|
||||
|
||||
if (child._parent) |parent| {
|
||||
// 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() });
|
||||
}
|
||||
|
||||
// Adopt the node tree if moving between documents
|
||||
if (adopting_to_new_document) {
|
||||
try page.adoptNodeTree(child, parent_owner);
|
||||
}
|
||||
|
||||
try page.appendNode(self, child, .{
|
||||
.child_already_connected = child_connected,
|
||||
.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;
|
||||
}
|
||||
|
||||
// Otherwise, this is a detached node. Check if it has a specific owner
|
||||
// document registered (for nodes created via non-main documents).
|
||||
if (page._node_owner_documents.get(@constCast(self))) |owner| {
|
||||
return owner;
|
||||
}
|
||||
|
||||
// Default to the main document for detached nodes without a specific owner.
|
||||
// Otherwise, this is a detached node. The owner is the document that
|
||||
// created it. For now, we only have one 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);
|
||||
};
|
||||
|
||||
// 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) {
|
||||
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);
|
||||
|
||||
const child_already_connected = new_node.isConnected();
|
||||
|
||||
// Check if we're adopting the node to a different document
|
||||
const child_owner = new_node.ownerDocument(page);
|
||||
const parent_owner = self.ownerDocument(page) orelse self.as(Document);
|
||||
const adopting_to_new_document = child_owner != null and child_owner.? != parent_owner;
|
||||
const child_root = new_node.getRootNode(null);
|
||||
const parent_root = self.getRootNode(null);
|
||||
const adopting_to_new_document = child_already_connected and child_root != parent_root;
|
||||
|
||||
page.domChanged();
|
||||
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 });
|
||||
}
|
||||
|
||||
// Adopt the node tree if moving between documents
|
||||
if (adopting_to_new_document) {
|
||||
try page.adoptNodeTree(new_node, parent_owner);
|
||||
}
|
||||
|
||||
try page.insertNodeRelative(
|
||||
self,
|
||||
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 self.insertBefore(new_child, old_child, page);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -871,14 +826,11 @@ pub const JsApi = struct {
|
||||
pub const ATTRIBUTE_NODE = bridge.property(2);
|
||||
pub const TEXT_NODE = bridge.property(3);
|
||||
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 COMMENT_NODE = bridge.property(8);
|
||||
pub const DOCUMENT_NODE = bridge.property(9);
|
||||
pub const DOCUMENT_TYPE_NODE = bridge.property(10);
|
||||
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_PRECEDING = bridge.property(0x02);
|
||||
@@ -934,6 +886,11 @@ pub const JsApi = struct {
|
||||
pub const getRootNode = bridge.function(Node.getRootNode, .{});
|
||||
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 {
|
||||
return page.base();
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ pub fn disconnect(self: *PerformanceObserver, page: *Page) void {
|
||||
/// Returns the current list of PerformanceEntry objects
|
||||
/// stored in the performance observer, emptying it out.
|
||||
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();
|
||||
return records;
|
||||
}
|
||||
|
||||
@@ -37,35 +37,22 @@ pub fn init(page: *Page) !*Range {
|
||||
}
|
||||
|
||||
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_offset = offset;
|
||||
|
||||
// If start is now after end, or nodes are in different trees, collapse to start
|
||||
const end_root = self._proto._end_container.getRootNode(null);
|
||||
const start_root = node.getRootNode(null);
|
||||
if (end_root != start_root or self._proto.isStartAfterEnd()) {
|
||||
// If start is now after end, collapse to start
|
||||
if (self._proto.isStartAfterEnd()) {
|
||||
self._proto._end_container = self._proto._start_container;
|
||||
self._proto._end_offset = self._proto._start_offset;
|
||||
}
|
||||
}
|
||||
|
||||
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_offset = offset;
|
||||
|
||||
// If end is now before start, or nodes are in different trees, collapse to end
|
||||
const start_root = self._proto._start_container.getRootNode(null);
|
||||
const end_root = node.getRootNode(null);
|
||||
if (start_root != end_root or self._proto.isStartAfterEnd()) {
|
||||
// If end is now before start, collapse to end
|
||||
if (self._proto.isStartAfterEnd()) {
|
||||
self._proto._start_container = self._proto._end_container;
|
||||
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 {
|
||||
const clone = try page._factory.abstractRange(Range{ ._proto = undefined }, page);
|
||||
clone._proto._end_offset = self._proto._end_offset;
|
||||
@@ -496,35 +308,24 @@ pub const JsApi = struct {
|
||||
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 setStart = bridge.function(Range.setStart, .{ .dom_exception = true });
|
||||
pub const setEnd = bridge.function(Range.setEnd, .{ .dom_exception = true });
|
||||
pub const setStartBefore = bridge.function(Range.setStartBefore, .{ .dom_exception = true });
|
||||
pub const setStartAfter = bridge.function(Range.setStartAfter, .{ .dom_exception = true });
|
||||
pub const setEndBefore = bridge.function(Range.setEndBefore, .{ .dom_exception = true });
|
||||
pub const setEndAfter = bridge.function(Range.setEndAfter, .{ .dom_exception = true });
|
||||
pub const selectNode = bridge.function(Range.selectNode, .{ .dom_exception = true });
|
||||
pub const setStart = bridge.function(Range.setStart, .{});
|
||||
pub const setEnd = bridge.function(Range.setEnd, .{});
|
||||
pub const setStartBefore = bridge.function(Range.setStartBefore, .{});
|
||||
pub const setStartAfter = bridge.function(Range.setStartAfter, .{});
|
||||
pub const setEndBefore = bridge.function(Range.setEndBefore, .{});
|
||||
pub const setEndAfter = bridge.function(Range.setEndAfter, .{});
|
||||
pub const selectNode = bridge.function(Range.selectNode, .{});
|
||||
pub const selectNodeContents = bridge.function(Range.selectNodeContents, .{});
|
||||
pub const collapse = bridge.function(Range.collapse, .{ .dom_exception = true });
|
||||
pub const detach = bridge.function(Range.detach, .{});
|
||||
pub const compareBoundaryPoints = bridge.function(Range.compareBoundaryPoints, .{ .dom_exception = true });
|
||||
pub const comparePoint = bridge.function(Range.comparePoint, .{ .dom_exception = true });
|
||||
pub const isPointInRange = bridge.function(Range.isPointInRange, .{ .dom_exception = true });
|
||||
pub const intersectsNode = bridge.function(Range.intersectsNode, .{});
|
||||
pub const cloneRange = bridge.function(Range.cloneRange, .{ .dom_exception = true });
|
||||
pub const insertNode = bridge.function(Range.insertNode, .{ .dom_exception = true });
|
||||
pub const deleteContents = bridge.function(Range.deleteContents, .{ .dom_exception = true });
|
||||
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 });
|
||||
pub const collapse = bridge.function(Range.collapse, .{});
|
||||
pub const cloneRange = bridge.function(Range.cloneRange, .{});
|
||||
pub const insertNode = bridge.function(Range.insertNode, .{});
|
||||
pub const deleteContents = bridge.function(Range.deleteContents, .{});
|
||||
pub const cloneContents = bridge.function(Range.cloneContents, .{});
|
||||
pub const extractContents = bridge.function(Range.extractContents, .{});
|
||||
pub const surroundContents = bridge.function(Range.surroundContents, .{});
|
||||
pub const createContextualFragment = bridge.function(Range.createContextualFragment, .{});
|
||||
pub const toString = bridge.function(Range.toString, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -39,7 +39,6 @@ _proto: *DocumentFragment,
|
||||
_mode: Mode,
|
||||
_host: *Element,
|
||||
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},
|
||||
_removed_ids: std.StringHashMapUnmanaged(void) = .{},
|
||||
|
||||
pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot {
|
||||
return page._factory.documentFragment(ShadowRoot{
|
||||
@@ -73,34 +72,9 @@ pub fn getHost(self: *const ShadowRoot) *Element {
|
||||
return self._host;
|
||||
}
|
||||
|
||||
pub fn getElementById(self: *ShadowRoot, id: []const u8, page: *Page) ?*Element {
|
||||
if (id.len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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 fn getElementById(self: *ShadowRoot, id_: ?[]const u8) ?*Element {
|
||||
const id = id_ orelse return null;
|
||||
return self._elements_by_id.get(id);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
@@ -114,17 +88,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const mode = bridge.accessor(ShadowRoot.getMode, null, .{});
|
||||
pub const host = bridge.accessor(ShadowRoot.getHost, null, .{});
|
||||
pub const getElementById = bridge.function(_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);
|
||||
}
|
||||
pub const getElementById = bridge.function(ShadowRoot.getElementById, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -28,6 +28,7 @@ const Mode = enum {
|
||||
tag,
|
||||
tag_name,
|
||||
class_name,
|
||||
name,
|
||||
all_elements,
|
||||
child_elements,
|
||||
child_tag,
|
||||
@@ -43,6 +44,7 @@ _data: union(Mode) {
|
||||
tag: NodeLive(.tag),
|
||||
tag_name: NodeLive(.tag_name),
|
||||
class_name: NodeLive(.class_name),
|
||||
name: NodeLive(.name),
|
||||
all_elements: NodeLive(.all_elements),
|
||||
child_elements: NodeLive(.child_elements),
|
||||
child_tag: NodeLive(.child_tag),
|
||||
@@ -77,6 +79,7 @@ pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {
|
||||
.tag => |*impl| .{ .tag = impl._tw.clone() },
|
||||
.tag_name => |*impl| .{ .tag_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() },
|
||||
.child_elements => |*impl| .{ .child_elements = impl._tw.clone() },
|
||||
.child_tag => |*impl| .{ .child_tag = impl._tw.clone() },
|
||||
@@ -95,6 +98,7 @@ pub const Iterator = GenericIterator(struct {
|
||||
tag: TreeWalker.FullExcludeSelf,
|
||||
tag_name: TreeWalker.FullExcludeSelf,
|
||||
class_name: TreeWalker.FullExcludeSelf,
|
||||
name: TreeWalker.FullExcludeSelf,
|
||||
all_elements: TreeWalker.FullExcludeSelf,
|
||||
child_elements: TreeWalker.Children,
|
||||
child_tag: TreeWalker.Children,
|
||||
@@ -109,6 +113,7 @@ pub const Iterator = GenericIterator(struct {
|
||||
.tag => |*impl| impl.nextTw(&self.tw.tag),
|
||||
.tag_name => |*impl| impl.nextTw(&self.tw.tag_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),
|
||||
.child_elements => |*impl| impl.nextTw(&self.tw.child_elements),
|
||||
.child_tag => |*impl| impl.nextTw(&self.tw.child_tag),
|
||||
|
||||
@@ -26,13 +26,11 @@ const Node = @import("../Node.zig");
|
||||
const ChildNodes = @import("ChildNodes.zig");
|
||||
const RadioNodeList = @import("RadioNodeList.zig");
|
||||
const SelectorList = @import("../selector/List.zig");
|
||||
const NodeLive = @import("node_live.zig").NodeLive;
|
||||
|
||||
const Mode = enum {
|
||||
child_nodes,
|
||||
selector_list,
|
||||
radio_node_list,
|
||||
name,
|
||||
};
|
||||
|
||||
const NodeList = @This();
|
||||
@@ -41,7 +39,6 @@ data: union(Mode) {
|
||||
child_nodes: *ChildNodes,
|
||||
selector_list: *SelectorList,
|
||||
radio_node_list: *RadioNodeList,
|
||||
name: NodeLive(.name),
|
||||
},
|
||||
|
||||
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),
|
||||
.selector_list => |impl| @intCast(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),
|
||||
.selector_list => |impl| impl.getAtIndex(index),
|
||||
.radio_node_list => |impl| impl.getAtIndex(index, page),
|
||||
.name => |*impl| if (impl.getAtIndex(index, page)) |el| el.asNode() else null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ pub fn NodeLive(comptime mode: Mode) type {
|
||||
}
|
||||
|
||||
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();
|
||||
if (self._tw.contains(node) and self.matches(node)) {
|
||||
return element;
|
||||
@@ -320,14 +320,12 @@ pub fn NodeLive(comptime mode: Mode) type {
|
||||
}
|
||||
|
||||
const HTMLCollection = @import("HTMLCollection.zig");
|
||||
const NodeList = @import("NodeList.zig");
|
||||
|
||||
pub fn runtimeGenericWrap(self: Self, page: *Page) !if (mode == .name) *NodeList else *HTMLCollection {
|
||||
pub fn runtimeGenericWrap(self: Self, page: *Page) !*HTMLCollection {
|
||||
const collection = switch (mode) {
|
||||
.name => return page._factory.create(NodeList{ .data = .{ .name = self } }),
|
||||
.tag => HTMLCollection{ ._data = .{ .tag = self } },
|
||||
.tag_name => HTMLCollection{ ._data = .{ .tag_name = self } },
|
||||
.class_name => HTMLCollection{ ._data = .{ .class_name = self } },
|
||||
.name => HTMLCollection{ ._data = .{ .name = self } },
|
||||
.all_elements => HTMLCollection{ ._data = .{ .all_elements = self } },
|
||||
.child_elements => HTMLCollection{ ._data = .{ .child_elements = self } },
|
||||
.child_tag => HTMLCollection{ ._data = .{ .child_tag = self } },
|
||||
|
||||
@@ -20,8 +20,6 @@ const std = @import("std");
|
||||
const log = @import("../../../log.zig");
|
||||
const String = @import("../../../string.zig").String;
|
||||
|
||||
const CssParser = @import("../../css/Parser.zig");
|
||||
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.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
|
||||
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| {
|
||||
const priority: ?[]const u8 = if (declaration.important) "important" else null;
|
||||
try self.setProperty(declaration.name, declaration.value, priority, page);
|
||||
const trimmed = std.mem.trim(u8, declaration, &std.ascii.whitespace);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -477,19 +477,11 @@ pub const NamedNodeMap = struct {
|
||||
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.
|
||||
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 {
|
||||
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 @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, null, null, .{ .null_as_undefined = true });
|
||||
pub const getNamedItem = bridge.function(NamedNodeMap.getByName, .{});
|
||||
pub const setNamedItem = bridge.function(NamedNodeMap.set, .{});
|
||||
pub const removeNamedItem = bridge.function(NamedNodeMap.removeByName, .{});
|
||||
pub const setNamedItem = bridge.function(NamedNodeMap.setByName, .{});
|
||||
pub const item = bridge.function(_item, .{});
|
||||
fn _item(self: *const NamedNodeMap, index: i32, page: *Page) !?*Attribute {
|
||||
// the bridge.indexed handles this, so if we want
|
||||
|
||||
@@ -220,18 +220,7 @@ pub const JsApi = struct {
|
||||
pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{});
|
||||
pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{});
|
||||
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, .{});
|
||||
|
||||
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");
|
||||
|
||||
@@ -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 (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);
|
||||
}
|
||||
// form attribute present but invalid - no form owner
|
||||
|
||||
@@ -46,16 +46,14 @@ pub const Type = enum {
|
||||
range,
|
||||
date,
|
||||
time,
|
||||
@"datetime-local",
|
||||
datetime_local,
|
||||
month,
|
||||
week,
|
||||
color,
|
||||
|
||||
pub fn fromString(str: []const u8) Type {
|
||||
// Longest type name is "datetime-local" at 14 chars
|
||||
if (str.len > 32) {
|
||||
return .text;
|
||||
}
|
||||
if (str.len > 32) return .text;
|
||||
|
||||
var buf: [32]u8 = undefined;
|
||||
const lower = std.ascii.lowerString(&buf, str);
|
||||
@@ -63,7 +61,10 @@ pub const Type = enum {
|
||||
}
|
||||
|
||||
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,
|
||||
_input_type: Type = .text,
|
||||
_selected: bool = false,
|
||||
_indeterminate: bool = false,
|
||||
|
||||
pub fn asElement(self: *Input) *Element {
|
||||
return self._proto._proto;
|
||||
@@ -129,14 +129,6 @@ pub fn setChecked(self: *Input, checked: bool, page: *Page) !void {
|
||||
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 {
|
||||
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 (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);
|
||||
}
|
||||
// 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 src = bridge.accessor(Input.getSrc, Input.setSrc, .{});
|
||||
pub const form = bridge.accessor(Input.getForm, null, .{});
|
||||
pub const indeterminate = bridge.accessor(Input.getIndeterminate, Input.setIndeterminate, .{});
|
||||
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");
|
||||
|
||||
@@ -68,16 +68,6 @@ pub const JsApi = struct {
|
||||
|
||||
pub const rel = bridge.accessor(Link.getRel, Link.setRel, .{});
|
||||
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");
|
||||
|
||||
@@ -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 (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);
|
||||
}
|
||||
// form attribute present but invalid - no form owner
|
||||
|
||||
@@ -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 (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);
|
||||
}
|
||||
// form attribute present but invalid - no form owner
|
||||
|
||||
11
src/html5ever/Cargo.lock
generated
11
src/html5ever/Cargo.lock
generated
@@ -72,7 +72,6 @@ dependencies = [
|
||||
"tikv-jemalloc-ctl",
|
||||
"tikv-jemallocator",
|
||||
"typed-arena",
|
||||
"xml5ever",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -477,13 +476,3 @@ name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "xml5ever"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee3f1e41afb31a75aef076563b0ad3ecc24f5bd9d12a72b132222664eb76b494"
|
||||
dependencies = [
|
||||
"log",
|
||||
"markup5ever",
|
||||
]
|
||||
|
||||
@@ -14,7 +14,6 @@ string_cache = "0.9.0"
|
||||
typed-arena = "2.0.2"
|
||||
tikv-jemallocator = {version = "0.6.0", features = ["stats"]}
|
||||
tikv-jemalloc-ctl = {version = "0.6.0", features = ["stats"]}
|
||||
xml5ever = "0.35.0"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
@@ -16,20 +16,20 @@
|
||||
// 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/>.
|
||||
|
||||
mod sink;
|
||||
mod types;
|
||||
mod sink;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[global_allocator]
|
||||
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
||||
|
||||
use types::*;
|
||||
use std::cell::Cell;
|
||||
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::tendril::{StrTendril, TendrilSink};
|
||||
use html5ever::{ns, parse_document, parse_fragment, LocalName, ParseOpts, Parser, QualName};
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn html5ever_parse_document(
|
||||
@@ -135,8 +135,7 @@ pub extern "C" fn html5ever_parse_fragment(
|
||||
|
||||
let bytes = unsafe { std::slice::from_raw_parts(html, len) };
|
||||
parse_fragment(
|
||||
sink,
|
||||
Default::default(),
|
||||
sink, Default::default(),
|
||||
QualName::new(None, ns!(html), LocalName::from("body")),
|
||||
vec![], // attributes
|
||||
false, // context_element_allows_scripting
|
||||
@@ -183,7 +182,7 @@ pub struct Memory {
|
||||
#[cfg(debug_assertions)]
|
||||
#[no_mangle]
|
||||
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.
|
||||
epoch::advance().unwrap();
|
||||
@@ -191,7 +190,7 @@ pub extern "C" fn html5ever_get_memory_usage() -> Memory {
|
||||
return Memory{
|
||||
resident: stats::resident::read().unwrap(),
|
||||
allocated: stats::allocated::read().unwrap(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Streaming parser API
|
||||
@@ -226,8 +225,9 @@ pub extern "C" fn html5ever_streaming_parser_create(
|
||||
// SAFETY: We're creating a self-referential structure here.
|
||||
// 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.
|
||||
let arena_ref: &'static typed_arena::Arena<sink::ElementData> =
|
||||
unsafe { std::mem::transmute(arena.as_ref()) };
|
||||
let arena_ref: &'static typed_arena::Arena<sink::ElementData> = unsafe {
|
||||
std::mem::transmute(arena.as_ref())
|
||||
};
|
||||
|
||||
let sink = sink::Sink {
|
||||
ctx: ctx,
|
||||
@@ -281,8 +281,7 @@ pub extern "C" fn html5ever_streaming_parser_feed(
|
||||
|
||||
// Feed the chunk to the parser
|
||||
// The Parser implements TendrilSink, so we can call process() on it
|
||||
let parser = streaming_parser
|
||||
.parser
|
||||
let parser = streaming_parser.parser
|
||||
.downcast_mut::<Parser<sink::Sink>>()
|
||||
.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) };
|
||||
|
||||
// Extract and finish the parser
|
||||
let parser = streaming_parser
|
||||
.parser
|
||||
let parser = streaming_parser.parser
|
||||
.downcast::<Parser<sink::Sink>>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user