58 Commits

Author SHA1 Message Date
Karl Seguin
060afcd459 Merge pull request #1313 from lightpanda-io/nikneym/xml-parsing
Support XML parsing
2026-01-08 08:42:21 +08:00
Karl Seguin
5d1522a61f Don't dispatch to listeners added during dispatching
Use the last current listener as a sentinel, so that any listener added during
dispatching can be skipped.
2026-01-08 08:39:49 +08:00
Karl Seguin
b1b54afc56 Merge pull request #1335 from lightpanda-io/build
Embed v8 snapshot in builds
2026-01-08 06:54:38 +08:00
Pierre Tachoire
2abc490732 ci: support build on tag push 2026-01-07 21:11:20 +01:00
Pierre Tachoire
d4807df2e9 add v8 snapshot instructions into the README 2026-01-07 17:22:50 +01:00
Pierre Tachoire
d5f4ca15cc add v8 snapshot in build processes 2026-01-07 17:22:49 +01:00
Pierre Tachoire
e642c85ebd use ReleaseFast build 2026-01-07 17:22:49 +01:00
Halil Durak
7ea0cdba36 update DomParser test 2026-01-07 14:37:44 +03:00
Halil Durak
612b3a26b7 allow other XML MIMEs in parseFromString 2026-01-07 14:37:44 +03:00
Halil Durak
56d89895a8 initial XML parsing support in DOMParser 2026-01-07 14:37:43 +03:00
Karl Seguin
21d502b81f Merge pull request #1326 from lightpanda-io/wp/mrdimidim/use-css-tokenizer
Use css tokenizer for parsing style attrs
2026-01-07 18:09:06 +08:00
Karl Seguin
dd3de6efea Merge pull request #1327 from lightpanda-io/zigdom-cdata-length
Fix `dom/nodes/CharacterData-appendData` WPT
2026-01-07 18:04:05 +08:00
Karl Seguin
d934fe6d4e Merge pull request #1330 from lightpanda-io/zigdom-element-get-by-class-name-fix
fix `dom/nodes/Element-getElementsByClassName` wpt
2026-01-07 18:02:50 +08:00
Karl Seguin
dab6345885 dispatch events with proper this 2026-01-07 17:57:34 +08:00
Karl Seguin
39874137d6 Merge pull request #1333 from lightpanda-io/attribute_removeNamedItem
add attribute.removeNamedItem
2026-01-07 17:47:33 +08:00
Karl Seguin
89f215c3ee Merge pull request #1332 from lightpanda-io/getElementById
getElementById duplicate-id handling
2026-01-07 17:47:19 +08:00
Karl Seguin
408d3f0a53 Track owning documents for nodes which aren't the default document
Track this in a lookup on the page, to avoid having to store a pointer for
_every_ node, given that most nodes _are_ owned by the document.

This helps us ensure nodes can be properly adopted.
2026-01-07 17:46:09 +08:00
Karl Seguin
a010684ce9 Add deprecated Node constants
Remove toString where the [new] auto-generated toString symbol works.

Reject node mutation on attributes.
2026-01-07 17:36:26 +08:00
Karl Seguin
a4a98da4a4 add attribute.removeNamedItem 2026-01-07 16:51:12 +08:00
Karl Seguin
6f30d459d5 getElementById duplicate-id handling
If 2 elements have the same id then,
1 - The first in document-order has to be retrieved. We were ordering by
    insertion order.

2 - When the element is removed, then document.getElementById should return the
    next element with that id in document-order. We were returning null
2026-01-07 15:49:17 +08:00
Muki Kiboigo
71f27a55e1 fix duping of string for getElementsByClassName 2026-01-06 23:07:32 -08:00
Karl Seguin
c92903aae5 Merge pull request #1328 from lightpanda-io/set_outerHTML
add outerHTML setter
2026-01-07 15:04:14 +08:00
Karl Seguin
518e0aa07a add outerHTML setter 2026-01-07 14:49:30 +08:00
Nikolay Govorov
b908b0bf8a Used css tokenizer for parse html attributes 2026-01-07 05:46:18 +00:00
Muki Kiboigo
f9fa5be324 count utf8 codepoints for CData getLength 2026-01-06 21:29:15 -08:00
Karl Seguin
8ec6bb1577 Merge pull request #1322 from lightpanda-io/input_clone
Add 'clone' callback to build, implement for Input
2026-01-07 13:08:20 +08:00
Karl Seguin
70f8c53703 add deprecated properties to Event and improve initEvent 2026-01-07 12:05:11 +08:00
Karl Seguin
6d5a984413 Merge pull request #1323 from lightpanda-io/document_title
Improve document.title getter
2026-01-07 10:42:03 +08:00
Karl Seguin
5fa8fbc6f8 Merge pull request #1324 from lightpanda-io/elements_by_name_nodelist
getElementsByName now returns a NodeList rather than an HTMLCollection
2026-01-07 10:41:54 +08:00
Karl Seguin
7050d5fc68 Merge pull request #1325 from lightpanda-io/tokenlist_treewalker
support element.relList and improve TreeWalker
2026-01-07 10:41:42 +08:00
Karl Seguin
6af9d12f71 support element.relList and improve TreeWalker 2026-01-07 10:34:47 +08:00
Karl Seguin
a54e1db784 getElementsByName now returns a NodeList rather than an HTMLCollection
Auto-implement a toString accessor for any type that has a JsApi.Meta.name
2026-01-07 09:17:51 +08:00
Karl Seguin
2319b0fda5 Improve document.title getter
Collapse whitespace and find the first title, no matter where it is.
2026-01-07 07:52:20 +08:00
Karl Seguin
6864a22721 fix datetime-local input type 2026-01-07 07:39:42 +08:00
Karl Seguin
c9d0e2097d add input indeterminate accessor 2026-01-07 07:35:24 +08:00
Karl Seguin
d8f7eb3f24 Add 'clone' callback to build, implement for Input 2026-01-07 07:29:43 +08:00
Karl Seguin
90ee919f45 Merge pull request #1321 from lightpanda-io/event-init
set _time_stamp in the Event factory
2026-01-07 07:14:34 +08:00
Karl Seguin
ddc6431720 Merge pull request #1316 from lightpanda-io/reject_non_new_constructor
Reject constructors called as function (i.e. without 'new')
2026-01-07 07:14:13 +08:00
Pierre Tachoire
2ea6557fb7 add initEvent into Factory
and remove default value for Event._time_stamp
2026-01-06 15:30:13 +01:00
Karl Seguin
15358c1928 Improve Range, adding missing functions and more validation 2026-01-06 20:27:16 +08:00
Karl Seguin
d65025b3cb Merge pull request #1320 from lightpanda-io/fix-replaceChildren
remove children from previous parent
2026-01-06 19:07:15 +08:00
Pierre Tachoire
54fa3bc054 remove children from previous parent 2026-01-06 11:52:47 +01:00
Pierre Tachoire
68f5fa738c remove dead code Page._appendNode 2026-01-06 11:49:13 +01:00
Karl Seguin
2ea57ba979 update v8 dep 2026-01-06 18:30:25 +08:00
Karl Seguin
1acc0b0dc8 Merge pull request #1310 from lightpanda-io/zigdom-named-access
Named Access on the Window Object
2026-01-06 18:07:43 +08:00
Karl Seguin
645ec79fce access page from context, document call_depth usage 2026-01-06 18:04:17 +08:00
Karl Seguin
97e897e80e Merge pull request #1318 from lightpanda-io/node-self-replace
handle Node self replacement in insertBefore
2026-01-06 17:23:12 +08:00
Karl Seguin
6f72eeae65 Merge pull request #1319 from lightpanda-io/script_list_cleanup
Handle immediate call to Script.errorCallback
2026-01-06 17:22:14 +08:00
Karl Seguin
a845b2e35e Handle immediate call to Script.errorCallback
It's possible for Script.errorCallback to be called as part of the call to
`client.request`. This happens because we eagerly pump the libcurl message loop
to get the request going ASAP. For very obvious failures (e.g. an invalid URL)
this means that the error callback can be called from `client.request`.

Previously, we were only adding the script to its list _after_ the call to
`client.request`, but the error handler tries to remove the script from the list
.

This commit changes the order so that the script is first added to the list
and then the request is made.
2026-01-06 17:03:27 +08:00
Pierre Tachoire
b164ffeb95 handle Node self replacement in insertBefore 2026-01-06 09:49:08 +01:00
Karl Seguin
7ba34af884 Merge pull request #1317 from lightpanda-io/zigValueToJs-opts-pass-down
pass down opts to zigValueToJs
2026-01-06 16:36:44 +08:00
Pierre Tachoire
7f543ac7c8 pass down opts to zigValueToJs 2026-01-06 09:35:38 +01:00
Karl Seguin
a1bf92c07f Reject constructors called as function (i.e. without 'new')
Previously, `MessageEvent('')` would have been allowed, but invalid. This caused
problems as the receiver was the window. All such calls are now rejected.

Depends on https://github.com/lightpanda-io/zig-v8-fork/pull/131
2026-01-06 16:03:37 +08:00
Karl Seguin
0b221615b7 Merge pull request #1315 from lightpanda-io/replaceChild-itself
fix Node.replaceChild when of new child equals old
2026-01-06 08:00:03 +08:00
Pierre Tachoire
f81a9b54a7 fix Node.replaceChild when of new child equals old 2026-01-05 21:48:59 +01:00
Muki Kiboigo
05da040ce1 increment call_depth on callWithThis 2026-01-05 09:27:17 -08:00
Muki Kiboigo
b911051842 add named access shadowing test 2026-01-05 09:08:57 -08:00
Muki Kiboigo
a67f46b550 add named access on the Window object 2026-01-05 08:41:42 -08:00
58 changed files with 2287 additions and 350 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

295
src/browser/css/Parser.zig Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -116,7 +116,29 @@ pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, a
pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T { pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype) !T {
const context = self.context; const context = self.context;
const js_this = try context.valueToExistingObject(this); // 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 aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args; const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,8 +19,13 @@
const std = @import("std"); const std = @import("std");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Parser = @import("../parser/Parser.zig");
const HTMLDocument = @import("HTMLDocument.zig"); const HTMLDocument = @import("HTMLDocument.zig");
const XMLDocument = @import("XMLDocument.zig");
const ProcessingInstruction = @import("../webapi/cdata/ProcessingInstruction.zig");
const DOMParser = @This(); const DOMParser = @This();
@@ -28,14 +33,27 @@ pub fn init() DOMParser {
return .{}; return .{};
} }
pub fn parseFromString(self: *const DOMParser, html: []const u8, mime_type: []const u8, page: *Page) !*HTMLDocument { pub const HTMLDocumentOrXMLDocument = union(enum) {
_ = self; html_document: *HTMLDocument,
xml_document: *XMLDocument,
};
// For now, only support text/html pub fn parseFromString(
if (!std.mem.eql(u8, mime_type, "text/html")) { _: *const DOMParser,
return error.NotSupported; 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);
if (maybe_target_mime) |target_mime| switch (target_mime) {
.@"text/html" => {
// Create a new HTMLDocument // Create a new HTMLDocument
const doc = try page._factory.document(HTMLDocument{ const doc = try page._factory.document(HTMLDocument{
._proto = undefined, ._proto = undefined,
@@ -47,7 +65,6 @@ pub fn parseFromString(self: *const DOMParser, html: []const u8, mime_type: []co
} }
// Parse HTML into the document // Parse HTML into the document
const Parser = @import("../parser/Parser.zig");
var parser = Parser.init(page.arena, doc.asNode(), page); var parser = Parser.init(page.arena, doc.asNode(), page);
parser.parse(normalized); parser.parse(normalized);
@@ -55,7 +72,39 @@ pub fn parseFromString(self: *const DOMParser, html: []const u8, mime_type: []co
return pe.err; return pe.err;
} }
return doc; 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;
} }
pub const JsApi = struct { pub const JsApi = struct {

View File

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

View File

@@ -48,6 +48,8 @@ _location: ?*Location = null,
_ready_state: ReadyState = .loading, _ready_state: ReadyState = .loading,
_current_script: ?*Element.Html.Script = null, _current_script: ?*Element.Html.Script = null,
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty, _elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty,
// Track IDs that were removed from the map - they might have duplicates in the tree
_removed_ids: std.StringHashMapUnmanaged(void) = .empty,
_active_element: ?*Element = null, _active_element: ?*Element = null,
_style_sheets: ?*StyleSheetList = null, _style_sheets: ?*StyleSheetList = null,
_write_insertion_point: ?*Node = null, _write_insertion_point: ?*Node = null,
@@ -121,10 +123,15 @@ const CreateElementOptions = struct {
is: ?[]const u8 = null, is: ?[]const u8 = null,
}; };
pub fn createElement(_: *const Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element { pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElementOptions, page: *Page) !*Element {
const node = try page.createElement(null, name, null); const node = try page.createElement(null, name, null);
const element = node.as(Element); const element = node.as(Element);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
const options = options_ orelse return element; const options = options_ orelse return element;
if (options.is) |is_value| { if (options.is) |is_value| {
try element.setAttribute("is", is_value, page); try element.setAttribute("is", is_value, page);
@@ -134,8 +141,13 @@ pub fn createElement(_: *const Document, name: []const u8, options_: ?CreateElem
return element; return element;
} }
pub fn createElementNS(_: *const Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element { pub fn createElementNS(self: *Document, namespace: ?[]const u8, name: []const u8, page: *Page) !*Element {
const node = try page.createElement(namespace, name, null); const node = try page.createElement(namespace, name, null);
// Track owner document if it's not the main document
if (self != page.document) {
try page.setNodeOwnerDocument(node, self);
}
return node.as(Element); return node.as(Element);
} }
@@ -163,9 +175,32 @@ pub fn createAttributeNS(_: *const Document, namespace: []const u8, name: []cons
}); });
} }
pub fn getElementById(self: *const Document, id_: ?[]const u8) ?*Element { pub fn getElementById(self: *Document, id: []const u8, page: *Page) ?*Element {
const id = id_ orelse return null; if (id.len == 0) {
return self._elements_by_id.get(id); 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;
} }
const GetElementsByTagNameResult = union(enum) { const GetElementsByTagNameResult = union(enum) {
@@ -252,28 +287,53 @@ pub fn getImplementation(_: *const Document) DOMImplementation {
return .{}; return .{};
} }
pub fn createDocumentFragment(_: *const Document, page: *Page) !*Node.DocumentFragment { pub fn createDocumentFragment(self: *Document, page: *Page) !*Node.DocumentFragment {
return Node.DocumentFragment.init(page); 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 createComment(_: *const Document, data: []const u8, page: *Page) !*Node { pub fn createComment(self: *Document, data: []const u8, page: *Page) !*Node {
return page.createComment(data); 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 createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node { pub fn createTextNode(self: *Document, data: []const u8, page: *Page) !*Node {
return page.createTextNode(data); 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 createCDATASection(self: *const Document, data: []const u8, page: *Page) !*Node { pub fn createCDATASection(self: *Document, data: []const u8, page: *Page) !*Node {
switch (self._type) { const node = switch (self._type) {
.html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument .html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument
.xml => return page.createCDATASection(data), .xml => try page.createCDATASection(data),
.generic => return 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);
} }
return node;
} }
pub fn createProcessingInstruction(_: *const Document, target: []const u8, data: []const u8, page: *Page) !*Node { pub fn createProcessingInstruction(self: *Document, target: []const u8, data: []const u8, page: *Page) !*Node {
return page.createProcessingInstruction(target, data); 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;
} }
const Range = @import("Range.zig"); const Range = @import("Range.zig");
@@ -301,14 +361,26 @@ pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@i
return error.NotSupported; return error.NotSupported;
} }
pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker { pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker {
const show = what_to_show orelse NodeFilter.SHOW_ALL; return DOMTreeWalker.init(root, try whatToShow(what_to_show), filter, page);
return DOMTreeWalker.init(root, show, filter, page);
} }
pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator { pub fn createNodeIterator(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMNodeIterator.FilterOpts, page: *Page) !*DOMNodeIterator {
const show = what_to_show orelse NodeFilter.SHOW_ALL; return DOMNodeIterator.init(root, try whatToShow(what_to_show), filter, page);
return DOMNodeIterator.init(root, show, filter, page); }
fn whatToShow(value_: ?js.Value) !u32 {
const value = value_ orelse return 4294967295; // show all when undefined
if (value.isUndefined()) {
// undefined explicitly passed
return 4294967295;
}
if (value.isNull()) {
return 0;
}
return value.toZig(u32);
} }
pub fn getReadyState(self: *const Document) []const u8 { pub fn getReadyState(self: *const Document) []const u8 {
@@ -673,7 +745,17 @@ pub const JsApi = struct {
pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true }); pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true });
pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{}); pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{});
pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{}); pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{});
pub const getElementById = bridge.function(Document.getElementById, .{}); 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 querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true }); pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true });
pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true }); pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true });
pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{}); pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,6 +39,7 @@ _proto: *DocumentFragment,
_mode: Mode, _mode: Mode,
_host: *Element, _host: *Element,
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .{}, _elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},
_removed_ids: std.StringHashMapUnmanaged(void) = .{},
pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot { pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot {
return page._factory.documentFragment(ShadowRoot{ return page._factory.documentFragment(ShadowRoot{
@@ -72,9 +73,34 @@ pub fn getHost(self: *const ShadowRoot) *Element {
return self._host; return self._host;
} }
pub fn getElementById(self: *ShadowRoot, id_: ?[]const u8) ?*Element { pub fn getElementById(self: *ShadowRoot, id: []const u8, page: *Page) ?*Element {
const id = id_ orelse return null; if (id.len == 0) {
return self._elements_by_id.get(id); 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 const JsApi = struct { pub const JsApi = struct {
@@ -88,7 +114,17 @@ pub const JsApi = struct {
pub const mode = bridge.accessor(ShadowRoot.getMode, null, .{}); pub const mode = bridge.accessor(ShadowRoot.getMode, null, .{});
pub const host = bridge.accessor(ShadowRoot.getHost, null, .{}); pub const host = bridge.accessor(ShadowRoot.getHost, null, .{});
pub const getElementById = bridge.function(ShadowRoot.getElementById, .{}); 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);
}
}; };
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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