mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-04-04 08:30:31 +00:00
Compare commits
199 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e15295bdac | ||
|
|
4e1f96e09c | ||
|
|
fdd52c17d7 | ||
|
|
07cefd71df | ||
|
|
abab10b2cc | ||
|
|
5fd95788f9 | ||
|
|
bd29f168e0 | ||
|
|
dc97e33cd6 | ||
|
|
caf7cb07cd | ||
|
|
ad5df53ee7 | ||
|
|
95920bf207 | ||
|
|
6700166841 | ||
|
|
b8196cd06e | ||
|
|
c28afbf193 | ||
|
|
b2c030140c | ||
|
|
92f131bbe4 | ||
|
|
deda53a842 | ||
|
|
5391854c82 | ||
|
|
e288bfbec4 | ||
|
|
377fe5bc40 | ||
|
|
d264ff2801 | ||
|
|
a21bb6b02d | ||
|
|
07a87dfba7 | ||
|
|
9e4db89521 | ||
|
|
536d394e41 | ||
|
|
c0580c7ad0 | ||
|
|
488e72ef4e | ||
|
|
01c224d301 | ||
|
|
eaf95a85a8 | ||
|
|
ba1d084660 | ||
|
|
2e64c461c3 | ||
|
|
ce5dad722f | ||
|
|
7675feca91 | ||
|
|
c66d74e135 | ||
|
|
54d6eed740 | ||
|
|
dc4b75070d | ||
|
|
830eb74725 | ||
|
|
4f21d8d7a8 | ||
|
|
424deb8faf | ||
|
|
b4a40f1257 | ||
|
|
9296c10ca4 | ||
|
|
fbe65cd542 | ||
|
|
ccbb6e4789 | ||
|
|
d70f436304 | ||
|
|
16aaa8201c | ||
|
|
acc1f2f3d7 | ||
|
|
433d254c70 | ||
|
|
ea4eebd2d6 | ||
|
|
3c00a527dd | ||
|
|
f72a354066 | ||
|
|
7c92e0e9ce | ||
|
|
4f6868728d | ||
|
|
0ec4522f9e | ||
|
|
c6e0c6d096 | ||
|
|
dc0fb9ed8a | ||
|
|
66d9eaee78 | ||
|
|
3797272faf | ||
|
|
682b302d04 | ||
|
|
1de10f9b05 | ||
|
|
89e38c34b8 | ||
|
|
246d17972c | ||
|
|
55a8b37ef8 | ||
|
|
445183001b | ||
|
|
ca9e2200da | ||
|
|
eba3f84c04 | ||
|
|
867e6a8f4b | ||
|
|
df9779ec59 | ||
|
|
1b71d1e46d | ||
|
|
0a58918f47 | ||
|
|
afbd927fc0 | ||
|
|
2aa09ae18d | ||
|
|
09789b0b72 | ||
|
|
2426abd17a | ||
|
|
db4a97743f | ||
|
|
7ca98ed344 | ||
|
|
c9d3d17999 | ||
|
|
628049cfd7 | ||
|
|
ae9a11da53 | ||
|
|
7e097482bc | ||
|
|
df1b151587 | ||
|
|
45eb59a5aa | ||
|
|
687c17bbe2 | ||
|
|
7505aec706 | ||
|
|
c7b414492d | ||
|
|
14b0095822 | ||
|
|
a1256b46c8 | ||
|
|
094270dff7 | ||
|
|
d4e24dabc2 | ||
|
|
842df0d112 | ||
|
|
cfa9427d7c | ||
|
|
3c01e24f02 | ||
|
|
22dbf63ff9 | ||
|
|
814f7394a0 | ||
|
|
9a4cebaa1b | ||
|
|
c30207ac63 | ||
|
|
77afbddb91 | ||
|
|
18feeabe15 | ||
|
|
c3811d3a14 | ||
|
|
f20d6b551d | ||
|
|
311bcadacb | ||
|
|
2189c8cd82 | ||
|
|
6553bb8147 | ||
|
|
dea492fd64 | ||
|
|
00ab7f04fa | ||
|
|
d3ba714aba | ||
|
|
748b37f1d6 | ||
|
|
b83b188aff | ||
|
|
cfefa32603 | ||
|
|
85d8db3ef9 | ||
|
|
3c14dbe382 | ||
|
|
b49b2af11f | ||
|
|
425a36aa51 | ||
|
|
ec0b9de713 | ||
|
|
9f13b14f6d | ||
|
|
01e83b45b5 | ||
|
|
f80566e0cb | ||
|
|
42afacf0af | ||
|
|
2e61e7e682 | ||
|
|
3de9267ea7 | ||
|
|
8c99d4fcd2 | ||
|
|
be4e6e5ba5 | ||
|
|
1b5efea6eb | ||
|
|
6554f80fad | ||
|
|
2e8a9f809e | ||
|
|
dc66032720 | ||
|
|
c9433782d8 | ||
|
|
fef5586ff5 | ||
|
|
1f4a2fd654 | ||
|
|
8243385af6 | ||
|
|
26ce9b2d4a | ||
|
|
119f3169e2 | ||
|
|
16bd22ee01 | ||
|
|
f4a5f73ab2 | ||
|
|
e61a4564ea | ||
|
|
e72edee1f2 | ||
|
|
e8c150fcac | ||
|
|
52418932b1 | ||
|
|
4f81cb9333 | ||
|
|
db46f47b96 | ||
|
|
edfe5594ba | ||
|
|
f25e972594 | ||
|
|
d5488bdd42 | ||
|
|
bbff64bc96 | ||
|
|
635afefdeb | ||
|
|
fd3e67a0b4 | ||
|
|
729a6021ee | ||
|
|
309f254c2c | ||
|
|
5c37f04d64 | ||
|
|
7c3dd8e852 | ||
|
|
66ddedbaf3 | ||
|
|
7981b17897 | ||
|
|
62137d47c8 | ||
|
|
e3b5437f61 | ||
|
|
934693924e | ||
|
|
308fd92a46 | ||
|
|
da1eb71ad0 | ||
|
|
576dbb7ce6 | ||
|
|
d0c381b3df | ||
|
|
55178a81c6 | ||
|
|
249308380b | ||
|
|
d91bec08c3 | ||
|
|
e23ef4b0be | ||
|
|
6037521c49 | ||
|
|
a27fac3677 | ||
|
|
21f2eb664e | ||
|
|
81546ef4b0 | ||
|
|
4b90c8fd45 | ||
|
|
c643fb8aac | ||
|
|
0cae6ceca3 | ||
|
|
5cde59b53c | ||
|
|
7df67630af | ||
|
|
0c89dca261 | ||
|
|
6b953b8793 | ||
|
|
0d1defcf27 | ||
|
|
c1db9c19b3 | ||
|
|
95487755ed | ||
|
|
4813469659 | ||
|
|
4dfd357c0b | ||
|
|
4ca0486518 | ||
|
|
b139c05960 | ||
|
|
3d32759030 | ||
|
|
badfe39a3d | ||
|
|
060e2db351 | ||
|
|
ed802c0404 | ||
|
|
5d8739bfb2 | ||
|
|
086faf44fc | ||
|
|
e5eaa90c61 | ||
|
|
b24807ea29 | ||
|
|
d68bae9bc2 | ||
|
|
b891fb4502 | ||
|
|
ea69b3b4e3 | ||
|
|
23c8616ba5 | ||
|
|
b25c91affd | ||
|
|
151cefe0ec | ||
|
|
3412ff94bc | ||
|
|
14112ed294 | ||
|
|
3e1909b645 | ||
|
|
a4b1fbd6ee | ||
|
|
6d2ef9be5d |
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
||||
zig-v8:
|
||||
description: 'zig v8 version to install'
|
||||
required: false
|
||||
default: 'v0.2.8'
|
||||
default: 'v0.2.9'
|
||||
v8:
|
||||
description: 'v8 version to install'
|
||||
required: false
|
||||
|
||||
53
.github/workflows/e2e-test.yml
vendored
53
.github/workflows/e2e-test.yml
vendored
@@ -122,10 +122,19 @@ jobs:
|
||||
needs: zig-build-release
|
||||
|
||||
env:
|
||||
MAX_MEMORY: 26000
|
||||
MAX_VmHWM: 28000 # 28MB (KB)
|
||||
MAX_CG_PEAK: 8000 # 8MB (KB)
|
||||
MAX_AVG_DURATION: 17
|
||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||
|
||||
# How to give cgroups access to the user actions-runner on the host:
|
||||
# $ sudo apt install cgroup-tools
|
||||
# $ sudo chmod o+w /sys/fs/cgroup/cgroup.procs
|
||||
# $ sudo mkdir -p /sys/fs/cgroup/actions-runner
|
||||
# $ sudo chown -R actions-runner:actions-runner /sys/fs/cgroup/actions-runner
|
||||
CG_ROOT: /sys/fs/cgroup
|
||||
CG: actions-runner/lpd_${{ github.run_id }}_${{ github.run_attempt }}
|
||||
|
||||
# use a self host runner.
|
||||
runs-on: lpd-bench-hetzner
|
||||
timeout-minutes: 15
|
||||
@@ -150,22 +159,53 @@ jobs:
|
||||
go run ws/main.go & echo $! > WS.pid
|
||||
sleep 2
|
||||
|
||||
- name: run lightpanda in cgroup
|
||||
run: |
|
||||
if [ ! -f /sys/fs/cgroup/cgroup.controllers ]; then
|
||||
echo "cgroup v2 not available: /sys/fs/cgroup/cgroup.controllers missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p $CG_ROOT/$CG
|
||||
cgexec -g memory:$CG ./lightpanda serve & echo $! > LPD.pid
|
||||
|
||||
sleep 2
|
||||
|
||||
- name: run puppeteer
|
||||
run: |
|
||||
./lightpanda serve & echo $! > LPD.pid
|
||||
sleep 2
|
||||
RUNS=100 npm run bench-puppeteer-cdp > puppeteer.out || exit 1
|
||||
cat /proc/`cat LPD.pid`/status |grep VmHWM|grep -oP '\d+' > LPD.VmHWM
|
||||
kill `cat LPD.pid`
|
||||
|
||||
PID=$(cat LPD.pid)
|
||||
while kill -0 $PID 2>/dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
if [ ! -f $CG_ROOT/$CG/memory.peak ]; then
|
||||
echo "memory.peak not available in $CG"
|
||||
exit 1
|
||||
fi
|
||||
cat $CG_ROOT/$CG/memory.peak > LPD.cg_mem_peak
|
||||
|
||||
- name: puppeteer result
|
||||
run: cat puppeteer.out
|
||||
|
||||
- name: memory regression
|
||||
- name: cgroup memory regression
|
||||
run: |
|
||||
PEAK_BYTES=$(cat LPD.cg_mem_peak)
|
||||
PEAK_KB=$((PEAK_BYTES / 1024))
|
||||
echo "memory.peak_bytes=$PEAK_BYTES"
|
||||
echo "memory.peak_kb=$PEAK_KB"
|
||||
test "$PEAK_KB" -le "$MAX_CG_PEAK"
|
||||
|
||||
- name: virtual memory regression
|
||||
run: |
|
||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||
echo "Peak resident set size: $LPD_VmHWM"
|
||||
test "$LPD_VmHWM" -le "$MAX_MEMORY"
|
||||
test "$LPD_VmHWM" -le "$MAX_VmHWM"
|
||||
|
||||
- name: cleanup cgroup
|
||||
run: rmdir $CG_ROOT/$CG
|
||||
|
||||
- name: duration regression
|
||||
run: |
|
||||
@@ -178,7 +218,8 @@ jobs:
|
||||
export AVG_DURATION=`cat puppeteer.out|grep 'avg run'|sed 's/avg run duration (ms) //'`
|
||||
export TOTAL_DURATION=`cat puppeteer.out|grep 'total duration'|sed 's/total duration (ms) //'`
|
||||
export LPD_VmHWM=`cat LPD.VmHWM`
|
||||
echo "{\"duration_total\":${TOTAL_DURATION},\"duration_avg\":${AVG_DURATION},\"mem_peak\":${LPD_VmHWM}}" > bench.json
|
||||
export LPD_CG_PEAK_KB=$(( $(cat LPD.cg_mem_peak) / 1024 ))
|
||||
echo "{\"duration_total\":${TOTAL_DURATION},\"duration_avg\":${AVG_DURATION},\"mem_peak\":${LPD_VmHWM},\"cg_mem_peak\":${LPD_CG_PEAK_KB}}" > bench.json
|
||||
cat bench.json
|
||||
|
||||
- name: run hyperfine
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM debian:stable-slim
|
||||
ARG MINISIG=0.12
|
||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||
ARG V8=14.0.365.4
|
||||
ARG ZIG_V8=v0.2.8
|
||||
ARG ZIG_V8=v0.2.9
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN apt-get update -yq && \
|
||||
|
||||
2
Makefile
2
Makefile
@@ -57,7 +57,7 @@ build-v8-snapshot:
|
||||
|
||||
## Build in release-fast mode
|
||||
build: build-v8-snapshot
|
||||
@printf "\033[36mBuilding (release safe)...\033[0m\n"
|
||||
@printf "\033[36mBuilding (release fast)...\033[0m\n"
|
||||
@$(ZIG) build -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dgit_commit=$$(git rev-parse --short HEAD) || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
||||
@printf "\033[33mBuild OK\033[0m\n"
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.2.8.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH63lfBADJ7UE2zAJ8nJIBnxoSiimXSaX6Q_M_7DS3",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.2.9.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH689vBACgpqFVEhT2wxRin-qQQSOcKJoM37MVo0rU",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
.@"boringssl-zig" = .{
|
||||
|
||||
@@ -29,6 +29,7 @@ free_list_len: u16 = 0,
|
||||
free_list: ?*Entry = null,
|
||||
free_list_max: u16,
|
||||
entry_pool: std.heap.MemoryPool(Entry),
|
||||
mutex: std.Thread.Mutex = .{},
|
||||
|
||||
const Entry = struct {
|
||||
next: ?*Entry,
|
||||
@@ -54,6 +55,9 @@ pub fn deinit(self: *ArenaPool) void {
|
||||
}
|
||||
|
||||
pub fn acquire(self: *ArenaPool) !Allocator {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
if (self.free_list) |entry| {
|
||||
self.free_list = entry.next;
|
||||
self.free_list_len -= 1;
|
||||
@@ -73,6 +77,12 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
||||
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||
const entry: *Entry = @fieldParentPtr("arena", arena);
|
||||
|
||||
// Reset the arena before acquiring the lock to minimize lock hold time
|
||||
_ = arena.reset(.{ .retain_with_limit = self.retain_bytes });
|
||||
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
const free_list_len = self.free_list_len;
|
||||
if (free_list_len == self.free_list_max) {
|
||||
arena.deinit();
|
||||
@@ -80,8 +90,12 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = arena.reset(.{ .retain_with_limit = self.retain_bytes });
|
||||
entry.next = self.free_list;
|
||||
self.free_list_len = free_list_len + 1;
|
||||
self.free_list = entry;
|
||||
}
|
||||
|
||||
pub fn reset(_: *const ArenaPool, allocator: Allocator, retain: usize) void {
|
||||
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
|
||||
_ = arena.reset(.{ .retain_with_limit = retain });
|
||||
}
|
||||
|
||||
108
src/Config.zig
108
src/Config.zig
@@ -30,6 +30,13 @@ pub const RunMode = enum {
|
||||
version,
|
||||
};
|
||||
|
||||
pub const CDP_MAX_HTTP_REQUEST_SIZE = 4096;
|
||||
|
||||
// max message size
|
||||
// +14 for max websocket payload overhead
|
||||
// +140 for the max control packet that might be interleaved in a message
|
||||
pub const CDP_MAX_MESSAGE_SIZE = 512 * 1024 + 14 + 140;
|
||||
|
||||
mode: Mode,
|
||||
exec_name: []const u8,
|
||||
http_headers: HttpHeaders,
|
||||
@@ -145,6 +152,20 @@ pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn maxConnections(self: *const Config) u16 {
|
||||
return switch (self.mode) {
|
||||
.serve => |opts| opts.cdp_max_connections,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn maxPendingConnections(self: *const Config) u31 {
|
||||
return switch (self.mode) {
|
||||
.serve => |opts| opts.cdp_max_pending_connections,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Mode = union(RunMode) {
|
||||
help: bool, // false when being printed because of an error
|
||||
fetch: Fetch,
|
||||
@@ -156,16 +177,19 @@ pub const Serve = struct {
|
||||
host: []const u8 = "127.0.0.1",
|
||||
port: u16 = 9222,
|
||||
timeout: u31 = 10,
|
||||
max_connections: u16 = 16,
|
||||
max_tabs_per_connection: u16 = 8,
|
||||
max_memory_per_tab: u64 = 512 * 1024 * 1024,
|
||||
max_pending_connections: u16 = 128,
|
||||
cdp_max_connections: u16 = 16,
|
||||
cdp_max_pending_connections: u16 = 128,
|
||||
common: Common = .{},
|
||||
};
|
||||
|
||||
pub const DumpFormat = enum {
|
||||
html,
|
||||
markdown,
|
||||
};
|
||||
|
||||
pub const Fetch = struct {
|
||||
url: [:0]const u8,
|
||||
dump: bool = false,
|
||||
dump_mode: ?DumpFormat = null,
|
||||
common: Common = .{},
|
||||
withbase: bool = false,
|
||||
strip: dump.Opts.Strip = .{},
|
||||
@@ -302,11 +326,12 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
\\
|
||||
\\fetch command
|
||||
\\Fetches the specified URL
|
||||
\\Example: {s} fetch --dump https://lightpanda.io/
|
||||
\\Example: {s} fetch --dump html https://lightpanda.io/
|
||||
\\
|
||||
\\Options:
|
||||
\\--dump Dumps document to stdout.
|
||||
\\ Defaults to false.
|
||||
\\ Argument must be 'html' or 'markdown'.
|
||||
\\ Defaults to no dump.
|
||||
\\
|
||||
\\--strip_mode Comma separated list of tag groups to remove from dump
|
||||
\\ the dump. e.g. --strip_mode js,css
|
||||
@@ -333,18 +358,11 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
|
||||
\\--timeout Inactivity timeout in seconds before disconnecting clients
|
||||
\\ Defaults to 10 (seconds). Limited to 604800 (1 week).
|
||||
\\
|
||||
\\--max_connections
|
||||
\\--cdp_max_connections
|
||||
\\ Maximum number of simultaneous CDP connections.
|
||||
\\ Defaults to 16.
|
||||
\\
|
||||
\\--max_tabs Maximum number of tabs per CDP connection.
|
||||
\\ Defaults to 8.
|
||||
\\
|
||||
\\--max_tab_memory
|
||||
\\ Maximum memory per tab in bytes.
|
||||
\\ Defaults to 536870912 (512 MB).
|
||||
\\
|
||||
\\--max_pending_connections
|
||||
\\--cdp_max_pending_connections
|
||||
\\ Maximum pending connections in the accept queue.
|
||||
\\ Defaults to 128.
|
||||
\\
|
||||
@@ -479,53 +497,27 @@ fn parseServeArgs(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--max_connections", opt)) {
|
||||
if (std.mem.eql(u8, "--cdp_max_connections", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_connections" });
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--cdp_max_connections" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.max_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_connections", .err = err });
|
||||
serve.cdp_max_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--cdp_max_connections", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--max_tabs", opt)) {
|
||||
if (std.mem.eql(u8, "--cdp_max_pending_connections", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_tabs" });
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--cdp_max_pending_connections" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.max_tabs_per_connection = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_tabs", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--max_tab_memory", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_tab_memory" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.max_memory_per_tab = std.fmt.parseInt(u64, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_tab_memory", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, "--max_pending_connections", opt)) {
|
||||
const str = args.next() orelse {
|
||||
log.fatal(.app, "missing argument value", .{ .arg = "--max_pending_connections" });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
|
||||
serve.max_pending_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--max_pending_connections", .err = err });
|
||||
serve.cdp_max_pending_connections = std.fmt.parseInt(u16, str, 10) catch |err| {
|
||||
log.fatal(.app, "invalid argument value", .{ .arg = "--cdp_max_pending_connections", .err = err });
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
continue;
|
||||
@@ -546,7 +538,7 @@ fn parseFetchArgs(
|
||||
allocator: Allocator,
|
||||
args: *std.process.ArgIterator,
|
||||
) !Fetch {
|
||||
var fetch_dump: bool = false;
|
||||
var dump_mode: ?DumpFormat = null;
|
||||
var withbase: bool = false;
|
||||
var url: ?[:0]const u8 = null;
|
||||
var common: Common = .{};
|
||||
@@ -554,7 +546,17 @@ fn parseFetchArgs(
|
||||
|
||||
while (args.next()) |opt| {
|
||||
if (std.mem.eql(u8, "--dump", opt)) {
|
||||
fetch_dump = true;
|
||||
var peek_args = args.*;
|
||||
if (peek_args.next()) |next_arg| {
|
||||
if (std.meta.stringToEnum(DumpFormat, next_arg)) |mode| {
|
||||
dump_mode = mode;
|
||||
_ = args.next();
|
||||
} else {
|
||||
dump_mode = .html;
|
||||
}
|
||||
} else {
|
||||
dump_mode = .html;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -621,7 +623,7 @@ fn parseFetchArgs(
|
||||
|
||||
return .{
|
||||
.url = url.?,
|
||||
.dump = fetch_dump,
|
||||
.dump_mode = dump_mode,
|
||||
.strip = strip,
|
||||
.common = common,
|
||||
.withbase = withbase,
|
||||
|
||||
359
src/Server.zig
359
src/Server.zig
@@ -28,23 +28,25 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
|
||||
const log = @import("log.zig");
|
||||
const App = @import("App.zig");
|
||||
const Config = @import("Config.zig");
|
||||
const CDP = @import("cdp/cdp.zig").CDP;
|
||||
|
||||
const MAX_HTTP_REQUEST_SIZE = 4096;
|
||||
|
||||
// max message size
|
||||
// +14 for max websocket payload overhead
|
||||
// +140 for the max control packet that might be interleaved in a message
|
||||
const MAX_MESSAGE_SIZE = 512 * 1024 + 14 + 140;
|
||||
const Http = @import("http/Http.zig");
|
||||
const HttpClient = @import("http/Client.zig");
|
||||
|
||||
const Server = @This();
|
||||
|
||||
app: *App,
|
||||
shutdown: bool = false,
|
||||
shutdown: std.atomic.Value(bool) = .init(false),
|
||||
allocator: Allocator,
|
||||
client: ?posix.socket_t,
|
||||
listener: ?posix.socket_t,
|
||||
json_version_response: []const u8,
|
||||
|
||||
// Thread management
|
||||
active_threads: std.atomic.Value(u32) = .init(0),
|
||||
clients: std.ArrayList(*Client) = .{},
|
||||
client_mutex: std.Thread.Mutex = .{},
|
||||
clients_pool: std.heap.MemoryPool(Client),
|
||||
|
||||
pub fn init(app: *App, address: net.Address) !Server {
|
||||
const allocator = app.allocator;
|
||||
const json_version_response = try buildJSONVersionResponse(allocator, address);
|
||||
@@ -52,19 +54,28 @@ pub fn init(app: *App, address: net.Address) !Server {
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.client = null,
|
||||
.listener = null,
|
||||
.allocator = allocator,
|
||||
.json_version_response = json_version_response,
|
||||
.clients_pool = std.heap.MemoryPool(Client).init(app.allocator),
|
||||
};
|
||||
}
|
||||
|
||||
/// Interrupts the server so that main can complete normally and call all defer handlers.
|
||||
pub fn stop(self: *Server) void {
|
||||
if (@atomicRmw(bool, &self.shutdown, .Xchg, true, .monotonic)) {
|
||||
if (self.shutdown.swap(true, .release)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Shutdown all active clients
|
||||
{
|
||||
self.client_mutex.lock();
|
||||
defer self.client_mutex.unlock();
|
||||
for (self.clients.items) |client| {
|
||||
client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
// Linux and BSD/macOS handle canceling a socket blocked on accept differently.
|
||||
// For Linux, we use std.shutdown, which will cause accept to return error.SocketNotListening (EINVAL).
|
||||
// For BSD, shutdown will return an error. Instead we call posix.close, which will result with error.ConnectionAborted (BADF).
|
||||
@@ -81,17 +92,22 @@ pub fn stop(self: *Server) void {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Server) void {
|
||||
if (!self.shutdown.load(.acquire)) {
|
||||
self.stop();
|
||||
}
|
||||
|
||||
self.joinThreads();
|
||||
if (self.listener) |listener| {
|
||||
posix.close(listener);
|
||||
self.listener = null;
|
||||
}
|
||||
// *if* server.run is running, we should really wait for it to return
|
||||
// before existing from here.
|
||||
self.clients.deinit(self.allocator);
|
||||
self.clients_pool.deinit();
|
||||
self.allocator.free(self.json_version_response);
|
||||
}
|
||||
|
||||
pub fn run(self: *Server, address: net.Address, timeout_ms: u32) !void {
|
||||
const flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC;
|
||||
const flags = posix.SOCK.STREAM | posix.SOCK.CLOEXEC | posix.SOCK.NONBLOCK;
|
||||
const listener = try posix.socket(address.any.family, flags, posix.IPPROTO.TCP);
|
||||
self.listener = listener;
|
||||
|
||||
@@ -101,16 +117,20 @@ pub fn run(self: *Server, address: net.Address, timeout_ms: u32) !void {
|
||||
}
|
||||
|
||||
try posix.bind(listener, &address.any, address.getOsSockLen());
|
||||
try posix.listen(listener, 1);
|
||||
try posix.listen(listener, self.app.config.maxPendingConnections());
|
||||
|
||||
log.info(.app, "server running", .{ .address = address });
|
||||
while (!@atomicLoad(bool, &self.shutdown, .monotonic)) {
|
||||
while (!self.shutdown.load(.acquire)) {
|
||||
const socket = posix.accept(listener, null, null, posix.SOCK.NONBLOCK) catch |err| {
|
||||
switch (err) {
|
||||
error.SocketNotListening, error.ConnectionAborted => {
|
||||
log.info(.app, "server stopped", .{});
|
||||
break;
|
||||
},
|
||||
error.WouldBlock => {
|
||||
std.Thread.sleep(10 * std.time.ns_per_ms);
|
||||
continue;
|
||||
},
|
||||
else => {
|
||||
log.err(.app, "CDP accept", .{ .err = err });
|
||||
std.Thread.sleep(std.time.ns_per_s);
|
||||
@@ -119,96 +139,121 @@ pub fn run(self: *Server, address: net.Address, timeout_ms: u32) !void {
|
||||
}
|
||||
};
|
||||
|
||||
self.client = socket;
|
||||
defer if (self.client) |s| {
|
||||
posix.close(s);
|
||||
self.client = null;
|
||||
};
|
||||
|
||||
if (log.enabled(.app, .info)) {
|
||||
var client_address: std.net.Address = undefined;
|
||||
var socklen: posix.socklen_t = @sizeOf(net.Address);
|
||||
try std.posix.getsockname(socket, &client_address.any, &socklen);
|
||||
log.info(.app, "client connected", .{ .ip = client_address });
|
||||
}
|
||||
|
||||
self.readLoop(socket, timeout_ms) catch |err| {
|
||||
log.err(.app, "CDP client loop", .{ .err = err });
|
||||
self.spawnWorker(socket, timeout_ms) catch |err| {
|
||||
log.err(.app, "CDP spawn", .{ .err = err });
|
||||
posix.close(socket);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn readLoop(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void {
|
||||
// This shouldn't be necessary, but the Client is HUGE (> 512KB) because
|
||||
// it has a large read buffer. I don't know why, but v8 crashes if this
|
||||
// is on the stack (and I assume it's related to its size).
|
||||
const client = try self.allocator.create(Client);
|
||||
defer self.allocator.destroy(client);
|
||||
fn handleConnection(self: *Server, socket: posix.socket_t, timeout_ms: u32) void {
|
||||
defer posix.close(socket);
|
||||
|
||||
client.* = try Client.init(socket, self);
|
||||
// Client is HUGE (> 512KB) because it has a large read buffer.
|
||||
// V8 crashes if this is on the stack (likely related to its size).
|
||||
const client = self.getClient() catch |err| {
|
||||
log.err(.app, "CDP client create", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
defer self.releaseClient(client);
|
||||
|
||||
client.* = Client.init(
|
||||
socket,
|
||||
self.allocator,
|
||||
self.app,
|
||||
self.json_version_response,
|
||||
timeout_ms,
|
||||
) catch |err| {
|
||||
log.err(.app, "CDP client init", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
defer client.deinit();
|
||||
|
||||
var http = &self.app.http;
|
||||
http.addCDPClient(.{
|
||||
.socket = socket,
|
||||
.ctx = client,
|
||||
.blocking_read_start = Client.blockingReadStart,
|
||||
.blocking_read = Client.blockingRead,
|
||||
.blocking_read_end = Client.blockingReadStop,
|
||||
});
|
||||
defer http.removeCDPClient();
|
||||
self.registerClient(client);
|
||||
defer self.unregisterClient(client);
|
||||
|
||||
lp.assert(client.mode == .http, "Server.readLoop invalid mode", .{});
|
||||
while (true) {
|
||||
if (http.poll(timeout_ms) != .cdp_socket) {
|
||||
log.info(.app, "CDP timeout", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
if (client.readSocket() == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client.mode == .cdp) {
|
||||
break; // switch to our CDP loop
|
||||
}
|
||||
// Check shutdown after registering to avoid missing stop() signal.
|
||||
// If stop() already iterated over clients, this client won't receive stop()
|
||||
// and would block joinThreads() indefinitely.
|
||||
if (self.shutdown.load(.acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cdp = &client.mode.cdp;
|
||||
var last_message = timestamp(.monotonic);
|
||||
var ms_remaining = timeout_ms;
|
||||
while (true) {
|
||||
switch (cdp.pageWait(ms_remaining)) {
|
||||
.cdp_socket => {
|
||||
if (client.readSocket() == false) {
|
||||
return;
|
||||
}
|
||||
last_message = timestamp(.monotonic);
|
||||
ms_remaining = timeout_ms;
|
||||
},
|
||||
.no_page => {
|
||||
if (http.poll(ms_remaining) != .cdp_socket) {
|
||||
log.info(.app, "CDP timeout", .{});
|
||||
return;
|
||||
}
|
||||
if (client.readSocket() == false) {
|
||||
return;
|
||||
}
|
||||
last_message = timestamp(.monotonic);
|
||||
ms_remaining = timeout_ms;
|
||||
},
|
||||
.done => {
|
||||
const elapsed = timestamp(.monotonic) - last_message;
|
||||
if (elapsed > ms_remaining) {
|
||||
log.info(.app, "CDP timeout", .{});
|
||||
return;
|
||||
}
|
||||
ms_remaining -= @intCast(elapsed);
|
||||
},
|
||||
client.start();
|
||||
}
|
||||
|
||||
fn getClient(self: *Server) !*Client {
|
||||
self.client_mutex.lock();
|
||||
defer self.client_mutex.unlock();
|
||||
return self.clients_pool.create();
|
||||
}
|
||||
|
||||
fn releaseClient(self: *Server, client: *Client) void {
|
||||
self.client_mutex.lock();
|
||||
defer self.client_mutex.unlock();
|
||||
self.clients_pool.destroy(client);
|
||||
}
|
||||
|
||||
fn registerClient(self: *Server, client: *Client) void {
|
||||
self.client_mutex.lock();
|
||||
defer self.client_mutex.unlock();
|
||||
self.clients.append(self.allocator, client) catch {};
|
||||
}
|
||||
|
||||
fn unregisterClient(self: *Server, client: *Client) void {
|
||||
self.client_mutex.lock();
|
||||
defer self.client_mutex.unlock();
|
||||
for (self.clients.items, 0..) |c, i| {
|
||||
if (c == client) {
|
||||
_ = self.clients.swapRemove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawnWorker(self: *Server, socket: posix.socket_t, timeout_ms: u32) !void {
|
||||
if (self.shutdown.load(.acquire)) {
|
||||
return error.ShuttingDown;
|
||||
}
|
||||
|
||||
// Atomically increment active_threads only if below max_connections.
|
||||
// Uses CAS loop to avoid race between checking the limit and incrementing.
|
||||
//
|
||||
// cmpxchgWeak may fail for two reasons:
|
||||
// 1. Another thread changed the value (increment or decrement)
|
||||
// 2. Spurious failure on some architectures (e.g. ARM)
|
||||
//
|
||||
// We use Weak instead of Strong because we need a retry loop anyway:
|
||||
// if CAS fails because a thread finished (counter decreased), we should
|
||||
// retry rather than return an error - there may now be room for a new connection.
|
||||
//
|
||||
// On failure, cmpxchgWeak returns the actual value, which we reuse to avoid
|
||||
// an extra load on the next iteration.
|
||||
const max_connections = self.app.config.maxConnections();
|
||||
var current = self.active_threads.load(.monotonic);
|
||||
while (current < max_connections) {
|
||||
current = self.active_threads.cmpxchgWeak(current, current + 1, .monotonic, .monotonic) orelse break;
|
||||
} else {
|
||||
return error.MaxThreadsReached;
|
||||
}
|
||||
errdefer _ = self.active_threads.fetchSub(1, .monotonic);
|
||||
|
||||
const thread = try std.Thread.spawn(.{}, runWorker, .{ self, socket, timeout_ms });
|
||||
thread.detach();
|
||||
}
|
||||
|
||||
fn runWorker(self: *Server, socket: posix.socket_t, timeout_ms: u32) void {
|
||||
defer _ = self.active_threads.fetchSub(1, .monotonic);
|
||||
handleConnection(self, socket, timeout_ms);
|
||||
}
|
||||
|
||||
fn joinThreads(self: *Server) void {
|
||||
while (self.active_threads.load(.monotonic) > 0) {
|
||||
std.Thread.sleep(10 * std.time.ns_per_ms);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle exactly one TCP connection.
|
||||
pub const Client = struct {
|
||||
// The client is initially serving HTTP requests but, under normal circumstances
|
||||
// should eventually be upgraded to a websocket connections
|
||||
@@ -217,11 +262,15 @@ pub const Client = struct {
|
||||
cdp: CDP,
|
||||
},
|
||||
|
||||
server: *Server,
|
||||
allocator: Allocator,
|
||||
app: *App,
|
||||
http: *HttpClient,
|
||||
json_version_response: []const u8,
|
||||
reader: Reader(true),
|
||||
socket: posix.socket_t,
|
||||
socket_flags: usize,
|
||||
send_arena: ArenaAllocator,
|
||||
timeout_ms: u32,
|
||||
|
||||
const EMPTY_PONG = [_]u8{ 138, 0 };
|
||||
|
||||
@@ -232,25 +281,49 @@ pub const Client = struct {
|
||||
// "private-use" close codes must be from 4000-49999
|
||||
const CLOSE_TIMEOUT = [_]u8{ 136, 2, 15, 160 }; // code: 4000
|
||||
|
||||
fn init(socket: posix.socket_t, server: *Server) !Client {
|
||||
fn init(
|
||||
socket: posix.socket_t,
|
||||
allocator: Allocator,
|
||||
app: *App,
|
||||
json_version_response: []const u8,
|
||||
timeout_ms: u32,
|
||||
) !Client {
|
||||
if (log.enabled(.app, .info)) {
|
||||
var client_address: std.net.Address = undefined;
|
||||
var socklen: posix.socklen_t = @sizeOf(net.Address);
|
||||
try std.posix.getsockname(socket, &client_address.any, &socklen);
|
||||
log.info(.app, "client connected", .{ .ip = client_address });
|
||||
}
|
||||
|
||||
const socket_flags = try posix.fcntl(socket, posix.F.GETFL, 0);
|
||||
const nonblocking = @as(u32, @bitCast(posix.O{ .NONBLOCK = true }));
|
||||
// we expect the socket to come to us as nonblocking
|
||||
lp.assert(socket_flags & nonblocking == nonblocking, "Client.init blocking", .{});
|
||||
|
||||
var reader = try Reader(true).init(server.allocator);
|
||||
var reader = try Reader(true).init(allocator);
|
||||
errdefer reader.deinit();
|
||||
|
||||
const http = try app.http.createClient(allocator);
|
||||
errdefer http.deinit();
|
||||
|
||||
return .{
|
||||
.socket = socket,
|
||||
.server = server,
|
||||
.allocator = allocator,
|
||||
.app = app,
|
||||
.http = http,
|
||||
.json_version_response = json_version_response,
|
||||
.reader = reader,
|
||||
.mode = .{ .http = {} },
|
||||
.socket_flags = socket_flags,
|
||||
.send_arena = ArenaAllocator.init(server.allocator),
|
||||
.send_arena = ArenaAllocator.init(allocator),
|
||||
.timeout_ms = timeout_ms,
|
||||
};
|
||||
}
|
||||
|
||||
fn stop(self: *Client) void {
|
||||
posix.shutdown(self.socket, .recv) catch {};
|
||||
}
|
||||
|
||||
fn deinit(self: *Client) void {
|
||||
switch (self.mode) {
|
||||
.cdp => |*cdp| cdp.deinit(),
|
||||
@@ -258,6 +331,88 @@ pub const Client = struct {
|
||||
}
|
||||
self.reader.deinit();
|
||||
self.send_arena.deinit();
|
||||
self.http.deinit();
|
||||
}
|
||||
|
||||
fn start(self: *Client) void {
|
||||
const http = self.http;
|
||||
http.cdp_client = .{
|
||||
.socket = self.socket,
|
||||
.ctx = self,
|
||||
.blocking_read_start = Client.blockingReadStart,
|
||||
.blocking_read = Client.blockingRead,
|
||||
.blocking_read_end = Client.blockingReadStop,
|
||||
};
|
||||
defer http.cdp_client = null;
|
||||
|
||||
self.httpLoop(http) catch |err| {
|
||||
log.err(.app, "CDP client loop", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
fn httpLoop(self: *Client, http: *HttpClient) !void {
|
||||
lp.assert(self.mode == .http, "Client.httpLoop invalid mode", .{});
|
||||
while (true) {
|
||||
const status = http.tick(self.timeout_ms) catch |err| {
|
||||
log.err(.app, "http tick", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
if (status != .cdp_socket) {
|
||||
log.info(.app, "CDP timeout", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.readSocket() == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.mode == .cdp) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return self.cdpLoop(http);
|
||||
}
|
||||
|
||||
fn cdpLoop(self: *Client, http: *HttpClient) !void {
|
||||
var cdp = &self.mode.cdp;
|
||||
var last_message = timestamp(.monotonic);
|
||||
var ms_remaining = self.timeout_ms;
|
||||
|
||||
while (true) {
|
||||
switch (cdp.pageWait(ms_remaining)) {
|
||||
.cdp_socket => {
|
||||
if (self.readSocket() == false) {
|
||||
return;
|
||||
}
|
||||
last_message = timestamp(.monotonic);
|
||||
ms_remaining = self.timeout_ms;
|
||||
},
|
||||
.no_page => {
|
||||
const status = http.tick(ms_remaining) catch |err| {
|
||||
log.err(.app, "http tick", .{ .err = err });
|
||||
return;
|
||||
};
|
||||
if (status != .cdp_socket) {
|
||||
log.info(.app, "CDP timeout", .{});
|
||||
return;
|
||||
}
|
||||
if (self.readSocket() == false) {
|
||||
return;
|
||||
}
|
||||
last_message = timestamp(.monotonic);
|
||||
ms_remaining = self.timeout_ms;
|
||||
},
|
||||
.done => {
|
||||
const elapsed = timestamp(.monotonic) - last_message;
|
||||
if (elapsed > ms_remaining) {
|
||||
log.info(.app, "CDP timeout", .{});
|
||||
return;
|
||||
}
|
||||
ms_remaining -= @intCast(elapsed);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn blockingReadStart(ctx: *anyopaque) bool {
|
||||
@@ -314,7 +469,7 @@ pub const Client = struct {
|
||||
lp.assert(self.reader.pos == 0, "Client.HTTP pos", .{ .pos = self.reader.pos });
|
||||
const request = self.reader.buf[0..self.reader.len];
|
||||
|
||||
if (request.len > MAX_HTTP_REQUEST_SIZE) {
|
||||
if (request.len > Config.CDP_MAX_HTTP_REQUEST_SIZE) {
|
||||
self.writeHTTPErrorResponse(413, "Request too large");
|
||||
return error.RequestTooLarge;
|
||||
}
|
||||
@@ -367,7 +522,7 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, url, "/json/version")) {
|
||||
try self.send(self.server.json_version_response);
|
||||
try self.send(self.json_version_response);
|
||||
// Chromedp (a Go driver) does an http request to /json/version
|
||||
// then to / (websocket upgrade) using a different connection.
|
||||
// Since we only allow 1 connection at a time, the 2nd one (the
|
||||
@@ -472,7 +627,7 @@ pub const Client = struct {
|
||||
break :blk res;
|
||||
};
|
||||
|
||||
self.mode = .{ .cdp = try CDP.init(self.server.app, self) };
|
||||
self.mode = .{ .cdp = try CDP.init(self.app, self.http, self) };
|
||||
return self.send(response);
|
||||
}
|
||||
|
||||
@@ -707,7 +862,7 @@ fn Reader(comptime EXPECT_MASK: bool) type {
|
||||
if (message_len > 125) {
|
||||
return error.ControlTooLarge;
|
||||
}
|
||||
} else if (message_len > MAX_MESSAGE_SIZE) {
|
||||
} else if (message_len > Config.CDP_MAX_MESSAGE_SIZE) {
|
||||
return error.TooLarge;
|
||||
} else if (message_len > self.buf.len) {
|
||||
const len = self.buf.len;
|
||||
@@ -735,7 +890,7 @@ fn Reader(comptime EXPECT_MASK: bool) type {
|
||||
|
||||
if (is_continuation) {
|
||||
const fragments = &(self.fragments orelse return error.InvalidContinuation);
|
||||
if (fragments.message.items.len + message_len > MAX_MESSAGE_SIZE) {
|
||||
if (fragments.message.items.len + message_len > Config.CDP_MAX_MESSAGE_SIZE) {
|
||||
return error.TooLarge;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const js = @import("js/js.zig");
|
||||
const log = @import("../log.zig");
|
||||
const App = @import("../App.zig");
|
||||
const HttpClient = @import("../http/Client.zig");
|
||||
|
||||
const ArenaPool = App.ArenaPool;
|
||||
const HttpClient = App.Http.Client;
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
@@ -44,13 +44,10 @@ session: ?Session,
|
||||
allocator: Allocator,
|
||||
arena_pool: *ArenaPool,
|
||||
http_client: *HttpClient,
|
||||
call_arena: ArenaAllocator,
|
||||
page_arena: ArenaAllocator,
|
||||
session_arena: ArenaAllocator,
|
||||
transfer_arena: ArenaAllocator,
|
||||
|
||||
const InitOpts = struct {
|
||||
env: js.Env.InitOpts = .{},
|
||||
http_client: *HttpClient,
|
||||
};
|
||||
|
||||
pub fn init(app: *App, opts: InitOpts) !Browser {
|
||||
@@ -65,21 +62,13 @@ pub fn init(app: *App, opts: InitOpts) !Browser {
|
||||
.session = null,
|
||||
.allocator = allocator,
|
||||
.arena_pool = &app.arena_pool,
|
||||
.http_client = app.http.client,
|
||||
.call_arena = ArenaAllocator.init(allocator),
|
||||
.page_arena = ArenaAllocator.init(allocator),
|
||||
.session_arena = ArenaAllocator.init(allocator),
|
||||
.transfer_arena = ArenaAllocator.init(allocator),
|
||||
.http_client = opts.http_client,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Browser) void {
|
||||
self.closeSession();
|
||||
self.env.deinit();
|
||||
self.call_arena.deinit();
|
||||
self.page_arena.deinit();
|
||||
self.session_arena.deinit();
|
||||
self.transfer_arena.deinit();
|
||||
}
|
||||
|
||||
pub fn newSession(self: *Browser, notification: *Notification) !*Session {
|
||||
@@ -94,7 +83,6 @@ pub fn closeSession(self: *Browser) void {
|
||||
if (self.session) |*session| {
|
||||
session.deinit();
|
||||
self.session = null;
|
||||
_ = self.session_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
self.env.memoryPressureNotification(.critical);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ const Page = @import("Page.zig");
|
||||
const Node = @import("webapi/Node.zig");
|
||||
const Event = @import("webapi/Event.zig");
|
||||
const EventTarget = @import("webapi/EventTarget.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
@@ -66,13 +67,13 @@ lookup: std.HashMapUnmanaged(
|
||||
dispatch_depth: usize,
|
||||
deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }),
|
||||
|
||||
pub fn init(page: *Page) EventManager {
|
||||
pub fn init(arena: Allocator, page: *Page) EventManager {
|
||||
return .{
|
||||
.page = page,
|
||||
.lookup = .{},
|
||||
.arena = page.arena,
|
||||
.list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena),
|
||||
.listener_pool = std.heap.MemoryPool(Listener).init(page.arena),
|
||||
.arena = arena,
|
||||
.list_pool = .init(arena),
|
||||
.listener_pool = .init(arena),
|
||||
.dispatch_depth = 0,
|
||||
.deferred_removals = .{},
|
||||
};
|
||||
@@ -254,20 +255,27 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
||||
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void {
|
||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||
|
||||
const page = self.page;
|
||||
const activation_state = ActivationState.create(event, target, page);
|
||||
|
||||
// Defer runs even on early return - ensures event phase is reset
|
||||
// and default actions execute (unless prevented)
|
||||
defer {
|
||||
event._event_phase = .none;
|
||||
// Handle checkbox/radio activation rollback or commit
|
||||
if (activation_state) |state| {
|
||||
state.restore(event, page);
|
||||
}
|
||||
|
||||
// Execute default action if not prevented
|
||||
if (event._prevent_default) {
|
||||
// can't return in a defer (╯°□°)╯︵ ┻━┻
|
||||
} else if (event._type_string.eqlSlice("click")) {
|
||||
self.page.handleClick(target) catch |err| {
|
||||
} else if (event._type_string.eql(comptime .wrap("click"))) {
|
||||
page.handleClick(target) catch |err| {
|
||||
log.warn(.event, "page.click", .{ .err = err });
|
||||
};
|
||||
} else if (event._type_string.eqlSlice("keydown")) {
|
||||
self.page.handleKeydown(target, event) catch |err| {
|
||||
} else if (event._type_string.eql(comptime .wrap("keydown"))) {
|
||||
page.handleKeydown(target, event) catch |err| {
|
||||
log.warn(.event, "page.keydown", .{ .err = err });
|
||||
};
|
||||
}
|
||||
@@ -302,7 +310,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
// Even though the window isn't part of the DOM, events always propagate
|
||||
// through it in the capture phase (unless we stopped at a shadow boundary)
|
||||
if (path_len < path_buffer.len) {
|
||||
path_buffer[path_len] = self.page.window.asEventTarget();
|
||||
path_buffer[path_len] = page.window.asEventTarget();
|
||||
path_len += 1;
|
||||
}
|
||||
|
||||
@@ -329,13 +337,36 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
|
||||
// Phase 2: At target
|
||||
event._event_phase = .at_target;
|
||||
const target_et = target.asEventTarget();
|
||||
if (self.lookup.get(.{
|
||||
.type_string = event._type_string,
|
||||
.event_target = @intFromPtr(target_et),
|
||||
})) |list| {
|
||||
try self.dispatchPhase(list, target_et, event, was_handled, null);
|
||||
if (event._stop_propagation) {
|
||||
return;
|
||||
|
||||
blk: {
|
||||
// Get inline handler (e.g., onclick property) for this target
|
||||
if (self.getInlineHandler(target_et, event)) |inline_handler| {
|
||||
was_handled.* = true;
|
||||
event._current_target = target_et;
|
||||
|
||||
var ls: js.Local.Scope = undefined;
|
||||
self.page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
try ls.toLocal(inline_handler).callWithThis(void, target_et, .{event});
|
||||
|
||||
if (event._stop_propagation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event._stop_immediate_propagation) {
|
||||
break :blk;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.lookup.get(.{
|
||||
.type_string = event._type_string,
|
||||
.event_target = @intFromPtr(target_et),
|
||||
})) |list| {
|
||||
try self.dispatchPhase(list, target_et, event, was_handled, null);
|
||||
if (event._stop_propagation) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,6 +491,19 @@ fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target:
|
||||
return self.dispatchPhase(list, current_target, event, was_handled, null);
|
||||
}
|
||||
|
||||
fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?js.Function.Global {
|
||||
const global_event_handlers = @import("webapi/global_event_handlers.zig");
|
||||
const handler_type = global_event_handlers.fromEventType(event._type_string.str()) orelse return null;
|
||||
|
||||
// Look up the inline handler for this target
|
||||
const element = switch (target._type) {
|
||||
.node => |n| n.is(Element) orelse return null,
|
||||
else => return null,
|
||||
};
|
||||
|
||||
return self.page.getAttrListener(element, handler_type);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -575,3 +619,144 @@ fn isAncestorOrSelf(ancestor: *Node, node: *Node) bool {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handles the default action for clicking on input checked/radio. Maybe this
|
||||
// could be generalized if needed, but I'm not sure. This wasn't obvious to me
|
||||
// but when an input is clicked, it's important to think about both the intent
|
||||
// and the actual result. Imagine you have an unchecked checkbox. When clicked,
|
||||
// the checkbox immediately becomes checked, and event handlers see this "checked"
|
||||
// intent. But a listener can preventDefault() in which case the check we did at
|
||||
// the start will be undone.
|
||||
// This is a bit more complicated for radio buttons, as the checking/unchecking
|
||||
// and the rollback can impact a different radio input. So if you "check" a radio
|
||||
// the intent is that it becomes checked and whatever was checked before becomes
|
||||
// unchecked, so that if you have to rollback (because of a preventDefault())
|
||||
// then both inputs have to revert to their original values.
|
||||
const ActivationState = struct {
|
||||
old_checked: bool,
|
||||
input: *Element.Html.Input,
|
||||
previously_checked_radio: ?*Input,
|
||||
|
||||
const Input = Element.Html.Input;
|
||||
|
||||
fn create(event: *const Event, target: *Node, page: *Page) ?ActivationState {
|
||||
if (event._type_string.eql(comptime .wrap("click")) == false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const input = target.is(Element.Html.Input) orelse return null;
|
||||
if (input._input_type != .checkbox and input._input_type != .radio) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const old_checked = input._checked;
|
||||
var previously_checked_radio: ?*Element.Html.Input = null;
|
||||
|
||||
// For radio buttons, find the currently checked radio in the group
|
||||
if (input._input_type == .radio and !old_checked) {
|
||||
previously_checked_radio = try findCheckedRadioInGroup(input, page);
|
||||
}
|
||||
|
||||
// Toggle checkbox or check radio (which unchecks others in group)
|
||||
const new_checked = if (input._input_type == .checkbox) !old_checked else true;
|
||||
try input.setChecked(new_checked, page);
|
||||
|
||||
return .{
|
||||
.input = input,
|
||||
.old_checked = old_checked,
|
||||
.previously_checked_radio = previously_checked_radio,
|
||||
};
|
||||
}
|
||||
|
||||
fn restore(self: *const ActivationState, event: *const Event, page: *Page) void {
|
||||
const input = self.input;
|
||||
if (event._prevent_default) {
|
||||
// Rollback: restore previous state
|
||||
input._checked = self.old_checked;
|
||||
input._checked_dirty = true;
|
||||
if (self.previously_checked_radio) |prev_radio| {
|
||||
prev_radio._checked = true;
|
||||
prev_radio._checked_dirty = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Commit: fire input and change events only if state actually changed
|
||||
// For checkboxes, state always changes. For radios, only if was unchecked.
|
||||
const state_changed = (input._input_type == .checkbox) or !self.old_checked;
|
||||
if (state_changed) {
|
||||
fireEvent(page, input, "input") catch |err| {
|
||||
log.warn(.event, "input event", .{ .err = err });
|
||||
};
|
||||
fireEvent(page, input, "change") catch |err| {
|
||||
log.warn(.event, "change event", .{ .err = err });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn findCheckedRadioInGroup(input: *Input, page: *Page) !?*Input {
|
||||
const elem = input.asElement();
|
||||
|
||||
const name = elem.getAttributeSafe(comptime .wrap("name")) orelse return null;
|
||||
if (name.len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const form = input.getForm(page);
|
||||
|
||||
// Walk from the root of the tree containing this element
|
||||
// This handles both document-attached and orphaned elements
|
||||
const root = elem.asNode().getRootNode(null);
|
||||
|
||||
const TreeWalker = @import("webapi/TreeWalker.zig");
|
||||
var walker = TreeWalker.Full.init(root, .{});
|
||||
|
||||
while (walker.next()) |node| {
|
||||
const other_element = node.is(Element) orelse continue;
|
||||
const other_input = other_element.is(Input) orelse continue;
|
||||
|
||||
if (other_input._input_type != .radio) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip the input we're checking from
|
||||
if (other_input == input) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const other_name = other_element.getAttributeSafe(comptime .wrap("name")) orelse continue;
|
||||
if (!std.mem.eql(u8, name, other_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if same form context
|
||||
const other_form = other_input.getForm(page);
|
||||
if (form) |f| {
|
||||
const of = other_form orelse continue;
|
||||
if (f != of) {
|
||||
continue; // Different forms
|
||||
}
|
||||
} else if (other_form != null) {
|
||||
continue; // form is null but other has a form
|
||||
}
|
||||
|
||||
if (other_input._checked) {
|
||||
return other_input;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fire input or change event
|
||||
fn fireEvent(page: *Page, input: *Input, comptime typ: []const u8) !void {
|
||||
const event = try Event.initTrusted(comptime .wrap(typ), .{
|
||||
.bubbles = true,
|
||||
.cancelable = false,
|
||||
}, page);
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
const target = input.asElement().asEventTarget();
|
||||
try page._event_manager.dispatch(target, event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -43,7 +43,9 @@ const IS_DEBUG = builtin.mode == .Debug;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const Factory = @This();
|
||||
|
||||
_page: *Page,
|
||||
_arena: Allocator,
|
||||
_slab: SlabAllocator,
|
||||
|
||||
fn PrototypeChain(comptime types: []const type) type {
|
||||
@@ -149,10 +151,11 @@ fn AutoPrototypeChain(comptime types: []const type) type {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(page: *Page) Factory {
|
||||
pub fn init(arena: Allocator, page: *Page) Factory {
|
||||
return .{
|
||||
._page = page,
|
||||
._slab = SlabAllocator.init(page.arena, 128),
|
||||
._arena = arena,
|
||||
._slab = SlabAllocator.init(arena, 128),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -172,6 +175,13 @@ pub fn eventTarget(self: *Factory, child: anytype) !*@TypeOf(child) {
|
||||
return chain.get(1);
|
||||
}
|
||||
|
||||
pub fn standaloneEventTarget(self: *Factory, child: anytype) !*EventTarget {
|
||||
const allocator = self._slab.allocator();
|
||||
const et = try allocator.create(EventTarget);
|
||||
et.* = .{ ._type = unionInit(EventTarget.Type, child) };
|
||||
return et;
|
||||
}
|
||||
|
||||
// this is a root object
|
||||
pub fn event(self: *Factory, arena: Allocator, typ: String, child: anytype) !*@TypeOf(child) {
|
||||
const chain = try PrototypeChain(
|
||||
@@ -329,7 +339,7 @@ pub fn svgElement(self: *Factory, tag_name: []const u8, child: anytype) !*@TypeO
|
||||
chain.setMiddle(2, Element.Type);
|
||||
|
||||
// will never allocate, can't fail
|
||||
const tag_name_str = String.init(self._page.arena, tag_name, .{}) catch unreachable;
|
||||
const tag_name_str = String.init(self._arena, tag_name, .{}) catch unreachable;
|
||||
|
||||
// Manually set Element.Svg with the tag_name
|
||||
chain.set(3, .{
|
||||
|
||||
@@ -38,6 +38,10 @@ pub const ContentTypeEnum = enum {
|
||||
text_javascript,
|
||||
text_plain,
|
||||
text_css,
|
||||
image_jpeg,
|
||||
image_gif,
|
||||
image_png,
|
||||
image_webp,
|
||||
application_json,
|
||||
unknown,
|
||||
other,
|
||||
@@ -49,6 +53,10 @@ pub const ContentType = union(ContentTypeEnum) {
|
||||
text_javascript: void,
|
||||
text_plain: void,
|
||||
text_css: void,
|
||||
image_jpeg: void,
|
||||
image_gif: void,
|
||||
image_png: void,
|
||||
image_webp: void,
|
||||
application_json: void,
|
||||
unknown: void,
|
||||
other: struct { type: []const u8, sub_type: []const u8 },
|
||||
@@ -61,6 +69,10 @@ pub fn contentTypeString(mime: *const Mime) []const u8 {
|
||||
.text_javascript => "application/javascript",
|
||||
.text_plain => "text/plain",
|
||||
.text_css => "text/css",
|
||||
.image_jpeg => "image/jpeg",
|
||||
.image_png => "image/png",
|
||||
.image_gif => "image/gif",
|
||||
.image_webp => "image/webp",
|
||||
.application_json => "application/json",
|
||||
else => "",
|
||||
};
|
||||
@@ -243,6 +255,11 @@ fn parseContentType(value: []const u8) !struct { ContentType, usize } {
|
||||
@"application/javascript",
|
||||
@"application/x-javascript",
|
||||
|
||||
@"image/jpeg",
|
||||
@"image/png",
|
||||
@"image/gif",
|
||||
@"image/webp",
|
||||
|
||||
@"application/json",
|
||||
}, type_name)) |known_type| {
|
||||
const ct: ContentType = switch (known_type) {
|
||||
@@ -251,6 +268,10 @@ fn parseContentType(value: []const u8) !struct { ContentType, usize } {
|
||||
.@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} },
|
||||
.@"text/plain" => .{ .text_plain = {} },
|
||||
.@"text/css" => .{ .text_css = {} },
|
||||
.@"image/jpeg" => .{ .image_jpeg = {} },
|
||||
.@"image/png" => .{ .image_png = {} },
|
||||
.@"image/gif" => .{ .image_gif = {} },
|
||||
.@"image/webp" => .{ .image_webp = {} },
|
||||
.@"application/json" => .{ .application_json = {} },
|
||||
};
|
||||
return .{ ct, attribute_start };
|
||||
@@ -358,6 +379,11 @@ test "Mime: parse common" {
|
||||
|
||||
try expect(.{ .content_type = .{ .application_json = {} } }, "application/json");
|
||||
try expect(.{ .content_type = .{ .text_css = {} } }, "text/css");
|
||||
|
||||
try expect(.{ .content_type = .{ .image_jpeg = {} } }, "image/jpeg");
|
||||
try expect(.{ .content_type = .{ .image_png = {} } }, "image/png");
|
||||
try expect(.{ .content_type = .{ .image_gif = {} } }, "image/gif");
|
||||
try expect(.{ .content_type = .{ .image_webp = {} } }, "image/webp");
|
||||
}
|
||||
|
||||
test "Mime: parse uncommon" {
|
||||
|
||||
@@ -83,7 +83,7 @@ _session: *Session,
|
||||
|
||||
_event_manager: EventManager,
|
||||
|
||||
_parse_mode: enum { document, fragment, document_write },
|
||||
_parse_mode: enum { document, fragment, document_write } = .document,
|
||||
|
||||
// See Attribute.List for what this is. TL;DR: proper DOM Attribute Nodes are
|
||||
// fat yet rarely needed. We only create them on-demand, but still need proper
|
||||
@@ -91,21 +91,22 @@ _parse_mode: enum { document, fragment, document_write },
|
||||
// a look here. We don't store this in the Element or Attribute.List.Entry
|
||||
// because that would require additional space per element / Attribute.List.Entry
|
||||
// even thoug we'll create very few (if any) actual *Attributes.
|
||||
_attribute_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute),
|
||||
_attribute_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute) = .empty,
|
||||
|
||||
// Same as _atlribute_lookup, but instead of individual attributes, this is for
|
||||
// the return of elements.attributes.
|
||||
_attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap),
|
||||
_attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap) = .empty,
|
||||
|
||||
// Lazily-created style, classList, and dataset objects. Only stored for elements
|
||||
// that actually access these features via JavaScript, saving 24 bytes per element.
|
||||
_element_styles: Element.StyleLookup = .{},
|
||||
_element_datasets: Element.DatasetLookup = .{},
|
||||
_element_class_lists: Element.ClassListLookup = .{},
|
||||
_element_rel_lists: Element.RelListLookup = .{},
|
||||
_element_shadow_roots: Element.ShadowRootLookup = .{},
|
||||
_node_owner_documents: Node.OwnerDocumentLookup = .{},
|
||||
_element_assigned_slots: Element.AssignedSlotLookup = .{},
|
||||
_element_styles: Element.StyleLookup = .empty,
|
||||
_element_datasets: Element.DatasetLookup = .empty,
|
||||
_element_class_lists: Element.ClassListLookup = .empty,
|
||||
_element_rel_lists: Element.RelListLookup = .empty,
|
||||
_element_shadow_roots: Element.ShadowRootLookup = .empty,
|
||||
_node_owner_documents: Node.OwnerDocumentLookup = .empty,
|
||||
_element_assigned_slots: Element.AssignedSlotLookup = .empty,
|
||||
_element_scroll_positions: Element.ScrollPositionLookup = .empty,
|
||||
|
||||
/// Lazily-created inline event listeners (or listeners provided as attributes).
|
||||
/// Avoids bloating all elements with extra function fields for rare usage.
|
||||
@@ -125,7 +126,7 @@ _element_assigned_slots: Element.AssignedSlotLookup = .{},
|
||||
/// ```js
|
||||
/// img.setAttribute("onload", "(() => { ... })()");
|
||||
/// ```
|
||||
_element_attr_listeners: GlobalEventHandlersLookup = .{},
|
||||
_element_attr_listeners: GlobalEventHandlersLookup = .empty,
|
||||
|
||||
/// `load` events that'll be fired before window's `load` event.
|
||||
/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it.
|
||||
@@ -167,9 +168,9 @@ _undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{},
|
||||
// for heap allocations and managing WebAPI objects
|
||||
_factory: Factory,
|
||||
|
||||
_load_state: LoadState,
|
||||
_load_state: LoadState = .waiting,
|
||||
|
||||
_parse_state: ParseState,
|
||||
_parse_state: ParseState = .pre,
|
||||
|
||||
_notified_network_idle: IdleNotification = .init,
|
||||
_notified_network_almost_idle: IdleNotification = .init,
|
||||
@@ -179,19 +180,19 @@ _notified_network_almost_idle: IdleNotification = .init,
|
||||
_queued_navigation: ?QueuedNavigation = null,
|
||||
|
||||
// The URL of the current page
|
||||
url: [:0]const u8,
|
||||
url: [:0]const u8 = "about:blank",
|
||||
|
||||
// The base url specifies the base URL used to resolve the relative urls.
|
||||
// It is set by a <base> tag.
|
||||
// If null the url must be used.
|
||||
base_url: ?[:0]const u8,
|
||||
base_url: ?[:0]const u8 = null,
|
||||
|
||||
// referer header cache.
|
||||
referer_header: ?[:0]const u8,
|
||||
referer_header: ?[:0]const u8 = null,
|
||||
|
||||
// Arbitrary buffer. Need to temporarily lowercase a value? Use this. No lifetime
|
||||
// guarantee - it's valid until someone else uses it.
|
||||
buf: [BUF_SIZE]u8,
|
||||
buf: [BUF_SIZE]u8 = undefined,
|
||||
|
||||
// access to the JavaScript engine
|
||||
js: *JS.Context,
|
||||
@@ -209,13 +210,13 @@ arena_pool: *ArenaPool,
|
||||
_arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct {
|
||||
owner: []const u8,
|
||||
count: usize,
|
||||
}) else void),
|
||||
}) else void) = if (IS_DEBUG) .empty else {},
|
||||
|
||||
window: *Window,
|
||||
document: *Document,
|
||||
|
||||
// DOM version used to invalidate cached state of "live" collections
|
||||
version: usize,
|
||||
version: usize = 0,
|
||||
|
||||
_req_id: ?usize = null,
|
||||
_navigated_options: ?NavigatedOpts = null,
|
||||
@@ -224,19 +225,63 @@ pub fn init(self: *Page, session: *Session) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.page, "page.init", .{});
|
||||
}
|
||||
|
||||
const browser = session.browser;
|
||||
self._session = session;
|
||||
const arena_pool = browser.arena_pool;
|
||||
const page_arena = try arena_pool.acquire();
|
||||
errdefer arena_pool.release(page_arena);
|
||||
|
||||
self.arena_pool = browser.arena_pool;
|
||||
self.arena = browser.page_arena.allocator();
|
||||
self.call_arena = browser.call_arena.allocator();
|
||||
const call_arena = try arena_pool.acquire();
|
||||
errdefer arena_pool.release(call_arena);
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
self._arena_pool_leak_track = .empty;
|
||||
var factory = Factory.init(page_arena, self);
|
||||
|
||||
const document = (try factory.document(Node.Document.HTMLDocument{
|
||||
._proto = undefined,
|
||||
})).asDocument();
|
||||
|
||||
self.* = .{
|
||||
.js = undefined,
|
||||
.arena = page_arena,
|
||||
.document = document,
|
||||
.window = undefined,
|
||||
.arena_pool = arena_pool,
|
||||
.call_arena = call_arena,
|
||||
._session = session,
|
||||
._factory = factory,
|
||||
._script_manager = undefined,
|
||||
._event_manager = EventManager.init(page_arena, self),
|
||||
};
|
||||
|
||||
self.window = try factory.eventTarget(Window{
|
||||
._proto = undefined,
|
||||
._document = self.document,
|
||||
._location = &default_location,
|
||||
._performance = Performance.init(),
|
||||
._screen = try factory.eventTarget(Screen{
|
||||
._proto = undefined,
|
||||
._orientation = null,
|
||||
}),
|
||||
._visual_viewport = try factory.eventTarget(VisualViewport{
|
||||
._proto = undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self);
|
||||
errdefer self._script_manager.deinit();
|
||||
|
||||
self.js = try browser.env.createContext(self, true);
|
||||
errdefer self.js.deinit();
|
||||
|
||||
if (comptime builtin.is_test == false) {
|
||||
// HTML test runner manually calls these as necessary
|
||||
try self.js.scheduler.add(session.browser, struct {
|
||||
fn runMessageLoop(ctx: *anyopaque) !?u32 {
|
||||
const b: *@import("Browser.zig") = @ptrCast(@alignCast(ctx));
|
||||
b.runMessageLoop();
|
||||
return 250;
|
||||
}
|
||||
}.runMessageLoop, 250, .{ .name = "page.messageLoop" });
|
||||
}
|
||||
|
||||
try self.reset(true);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Page) void {
|
||||
@@ -265,132 +310,15 @@ pub fn deinit(self: *Page) void {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(self: *Page, comptime initializing: bool) !void {
|
||||
const browser = self._session.browser;
|
||||
|
||||
if (comptime initializing == false) {
|
||||
browser.env.destroyContext(self.js);
|
||||
|
||||
// We force a garbage collection between page navigations to keep v8
|
||||
// memory usage as low as possible.
|
||||
browser.env.memoryPressureNotification(.moderate);
|
||||
self._script_manager.shutdown = true;
|
||||
browser.http_client.abort();
|
||||
self._script_manager.deinit();
|
||||
|
||||
// destroying the context, and aborting the http_client can both cause
|
||||
// resources to be freed. We need to check for a leak after we've finished
|
||||
// all of our cleanup.
|
||||
if (comptime IS_DEBUG) {
|
||||
var it = self._arena_pool_leak_track.valueIterator();
|
||||
while (it.next()) |value_ptr| {
|
||||
if (value_ptr.count > 0) {
|
||||
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner });
|
||||
}
|
||||
}
|
||||
self._arena_pool_leak_track = .empty;
|
||||
}
|
||||
|
||||
_ = browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
}
|
||||
|
||||
self._factory = Factory.init(self);
|
||||
|
||||
self.version = 0;
|
||||
self.url = "about:blank";
|
||||
self.base_url = null;
|
||||
self.referer_header = null;
|
||||
|
||||
self.document = (try self._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument();
|
||||
|
||||
const storage_bucket = try self._factory.create(storage.Bucket{});
|
||||
const screen = try Screen.init(self);
|
||||
const visual_viewport = try VisualViewport.init(self);
|
||||
self.window = try self._factory.eventTarget(Window{
|
||||
._document = self.document,
|
||||
._storage_bucket = storage_bucket,
|
||||
._performance = Performance.init(),
|
||||
._proto = undefined,
|
||||
._location = &default_location,
|
||||
._screen = screen,
|
||||
._visual_viewport = visual_viewport,
|
||||
});
|
||||
self.window._document = self.document;
|
||||
self.window._location = &default_location;
|
||||
|
||||
self._parse_state = .pre;
|
||||
self._load_state = .waiting;
|
||||
self._queued_navigation = null;
|
||||
self._parse_mode = .document;
|
||||
self._attribute_lookup = .empty;
|
||||
self._attribute_named_node_map_lookup = .empty;
|
||||
self._event_manager = EventManager.init(self);
|
||||
|
||||
self._script_manager = ScriptManager.init(self);
|
||||
errdefer self._script_manager.deinit();
|
||||
|
||||
self.js = try browser.env.createContext(self, true);
|
||||
errdefer self.js.deinit();
|
||||
|
||||
self._element_styles = .{};
|
||||
self._element_datasets = .{};
|
||||
self._element_class_lists = .{};
|
||||
self._element_rel_lists = .{};
|
||||
self._element_shadow_roots = .{};
|
||||
self._node_owner_documents = .{};
|
||||
self._element_assigned_slots = .{};
|
||||
|
||||
self._element_attr_listeners = .{};
|
||||
|
||||
self._to_load = .{};
|
||||
|
||||
self._notified_network_idle = .init;
|
||||
self._notified_network_almost_idle = .init;
|
||||
|
||||
self._performance_observers = .{};
|
||||
self._mutation_observers = .{};
|
||||
self._mutation_delivery_scheduled = false;
|
||||
self._mutation_delivery_depth = 0;
|
||||
self._intersection_observers = .{};
|
||||
self._intersection_check_scheduled = false;
|
||||
self._intersection_delivery_scheduled = false;
|
||||
self._slots_pending_slotchange = .{};
|
||||
self._slotchange_delivery_scheduled = false;
|
||||
self._customized_builtin_definitions = .{};
|
||||
self._customized_builtin_connected_callback_invoked = .{};
|
||||
self._customized_builtin_disconnected_callback_invoked = .{};
|
||||
self._undefined_custom_elements = .{};
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
self._arena_pool_leak_track = .{};
|
||||
}
|
||||
|
||||
try self.registerBackgroundTasks();
|
||||
self.arena_pool.release(self.call_arena);
|
||||
self.arena_pool.release(self.arena);
|
||||
}
|
||||
|
||||
pub fn base(self: *const Page) [:0]const u8 {
|
||||
return self.base_url orelse self.url;
|
||||
}
|
||||
|
||||
fn registerBackgroundTasks(self: *Page) !void {
|
||||
if (comptime builtin.is_test) {
|
||||
// HTML test runner manually calls these as necessary
|
||||
return;
|
||||
}
|
||||
|
||||
const Browser = @import("Browser.zig");
|
||||
|
||||
try self.js.scheduler.add(self._session.browser, struct {
|
||||
fn runMessageLoop(ctx: *anyopaque) !?u32 {
|
||||
const b: *Browser = @ptrCast(@alignCast(ctx));
|
||||
b.runMessageLoop();
|
||||
return 250;
|
||||
}
|
||||
}.runMessageLoop, 250, .{ .name = "page.messageLoop" });
|
||||
}
|
||||
|
||||
pub fn getTitle(self: *Page) !?[]const u8 {
|
||||
if (self.window._document.is(Document.HTMLDocument)) |html_doc| {
|
||||
return try html_doc.getTitle(self);
|
||||
@@ -461,12 +389,8 @@ pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
|
||||
}
|
||||
|
||||
pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
|
||||
lp.assert(self._load_state == .waiting, "page.renavigate", .{});
|
||||
const session = self._session;
|
||||
if (self._parse_state != .pre or self._load_state != .waiting) {
|
||||
// it's possible for navigate to be called multiple times on the
|
||||
// same page (via CDP). We want to reset the page between each call.
|
||||
try self.reset(false);
|
||||
}
|
||||
self._load_state = .parsing;
|
||||
|
||||
const req_id = self._session.browser.http_client.nextReqId();
|
||||
@@ -710,18 +634,6 @@ fn _documentIsComplete(self: *Page) !void {
|
||||
for (self._to_load.items) |element| {
|
||||
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
||||
defer if (!event._v8_handoff) event.deinit(false);
|
||||
|
||||
// Dispatch inline event.
|
||||
blk: {
|
||||
const html_element = element.is(HtmlElement) orelse break :blk;
|
||||
|
||||
const listener = (try html_element.getOnLoad(self)) orelse break :blk;
|
||||
ls.toLocal(listener).call(void, .{}) catch |err| {
|
||||
log.warn(.event, "inline load event", .{ .element = element, .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
// Dispatch events registered to event manager.
|
||||
try self._event_manager.dispatch(element.asEventTarget(), event);
|
||||
}
|
||||
|
||||
@@ -796,7 +708,10 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
try arr.appendSlice(self.arena, "<html><head><meta charset=\"utf-8\"></head><body><pre>");
|
||||
self._parse_state = .{ .text = arr };
|
||||
},
|
||||
else => self._parse_state = .{ .raw = .{} },
|
||||
.image_jpeg, .image_gif, .image_png, .image_webp => {
|
||||
self._parse_state = .{ .image = .empty };
|
||||
},
|
||||
else => self._parse_state = .{ .raw = .empty },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -818,7 +733,7 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
v = v[index + 1 ..];
|
||||
}
|
||||
},
|
||||
.raw => |*buf| try buf.appendSlice(self.arena, data),
|
||||
.raw, .image => |*buf| try buf.appendSlice(self.arena, data),
|
||||
.pre => unreachable,
|
||||
.complete => unreachable,
|
||||
.err => unreachable,
|
||||
@@ -841,12 +756,13 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
|
||||
log.debug(.page, "page.load.complete", .{ .url = self.url });
|
||||
};
|
||||
|
||||
const parse_arena = try self.getArena(.{ .debug = "Page.parse" });
|
||||
defer self.releaseArena(parse_arena);
|
||||
|
||||
var parser = Parser.init(parse_arena, self.document.asNode(), self);
|
||||
|
||||
switch (self._parse_state) {
|
||||
.html => |buf| {
|
||||
const parse_arena = try self.getArena(.{ .debug = "Page.parse" });
|
||||
defer self.releaseArena(parse_arena);
|
||||
|
||||
var parser = Parser.init(parse_arena, self.document.asNode(), self);
|
||||
parser.parse(buf.items);
|
||||
self._script_manager.staticScriptsDone();
|
||||
if (self._script_manager.isDone()) {
|
||||
@@ -858,16 +774,26 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
|
||||
},
|
||||
.text => |*buf| {
|
||||
try buf.appendSlice(self.arena, "</pre></body></html>");
|
||||
|
||||
const parse_arena = try self.getArena(.{ .debug = "Page.parse" });
|
||||
defer self.releaseArena(parse_arena);
|
||||
|
||||
var parser = Parser.init(parse_arena, self.document.asNode(), self);
|
||||
parser.parse(buf.items);
|
||||
self.documentIsComplete();
|
||||
},
|
||||
.image => |buf| {
|
||||
self._parse_state = .{ .raw_done = buf.items };
|
||||
|
||||
// Use empty an HTML containing the image.
|
||||
const html = try std.mem.concat(parse_arena, u8, &.{
|
||||
"<html><head><meta charset=\"utf-8\"></head><body><img src=\"",
|
||||
self.url,
|
||||
"\"></body></htm>",
|
||||
});
|
||||
parser.parse(html);
|
||||
self.documentIsComplete();
|
||||
},
|
||||
.raw => |buf| {
|
||||
self._parse_state = .{ .raw_done = buf.items };
|
||||
|
||||
// Use empty an empty HTML document.
|
||||
parser.parse("<html><head><meta charset=\"utf-8\"></head><body></body></htm>");
|
||||
self.documentIsComplete();
|
||||
},
|
||||
.pre => {
|
||||
@@ -875,6 +801,20 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
|
||||
// We assume we have received an OK status (checked in Client.headerCallback)
|
||||
// so we load a blank document to navigate away from any prior page.
|
||||
self._parse_state = .{ .complete = {} };
|
||||
|
||||
// Use empty an empty HTML document.
|
||||
parser.parse("<html><head><meta charset=\"utf-8\"></head><body></body></htm>");
|
||||
self.documentIsComplete();
|
||||
},
|
||||
.err => |err| {
|
||||
// Generate a pseudo HTML page indicating the failure.
|
||||
const html = try std.mem.concat(parse_arena, u8, &.{
|
||||
"<html><head><meta charset=\"utf-8\"></head><body><h1>Navigation failed</h1><p>Reason: ",
|
||||
@errorName(err),
|
||||
"</p></body></htm>",
|
||||
});
|
||||
|
||||
parser.parse(html);
|
||||
self.documentIsComplete();
|
||||
},
|
||||
else => unreachable,
|
||||
@@ -885,8 +825,14 @@ fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
log.err(.page, "navigate failed", .{ .err = err });
|
||||
|
||||
var self: *Page = @ptrCast(@alignCast(ctx));
|
||||
self.clearTransferArena();
|
||||
self._parse_state = .{ .err = err };
|
||||
|
||||
// In case of error, we want to complete the page with a custom HTML
|
||||
// containing the error.
|
||||
pageDoneCallback(ctx) catch |e| {
|
||||
log.err(.browser, "pageErrorCallback", .{ .err = e });
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
// The transfer arena is useful and interesting, but has a weird lifetime.
|
||||
@@ -903,7 +849,7 @@ fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
// why would we want to) and requires the body to live until the transfer
|
||||
// is complete.
|
||||
fn clearTransferArena(self: *Page) void {
|
||||
_ = self._session.browser.transfer_arena.reset(.{ .retain_with_limit = 4 * 1024 });
|
||||
self.arena_pool.reset(self._session.transfer_arena, 4 * 1024);
|
||||
}
|
||||
|
||||
pub fn wait(self: *Page, wait_ms: u32) Session.WaitResult {
|
||||
@@ -947,7 +893,7 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
|
||||
|
||||
while (true) {
|
||||
switch (self._parse_state) {
|
||||
.pre, .raw, .text => {
|
||||
.pre, .raw, .text, .image => {
|
||||
// The main page hasn't started/finished navigating.
|
||||
// There's no JS to run, and no reason to run the scheduler.
|
||||
if (http_client.active == 0 and exit_when_done) {
|
||||
@@ -1254,8 +1200,10 @@ pub fn setAttrListener(
|
||||
});
|
||||
}
|
||||
|
||||
const key = global_event_handlers.calculateKey(element.asEventTarget(), listener_type);
|
||||
const gop = try self._element_attr_listeners.getOrPut(self.arena, key);
|
||||
const gop = try self._element_attr_listeners.getOrPut(self.arena, .{
|
||||
.target = element.asEventTarget(),
|
||||
.handler = listener_type,
|
||||
});
|
||||
gop.value_ptr.* = listener_callback;
|
||||
}
|
||||
|
||||
@@ -1265,8 +1213,10 @@ pub fn getAttrListener(
|
||||
element: *Element,
|
||||
listener_type: GlobalEventHandler,
|
||||
) ?JS.Function.Global {
|
||||
const key = global_event_handlers.calculateKey(element.asEventTarget(), listener_type);
|
||||
return self._element_attr_listeners.get(key);
|
||||
return self._element_attr_listeners.get(.{
|
||||
.target = element.asEventTarget(),
|
||||
.handler = listener_type,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void {
|
||||
@@ -1293,6 +1243,11 @@ pub fn notifyPerformanceObservers(self: *Page, entry: *Performance.Entry) !void
|
||||
}
|
||||
}
|
||||
|
||||
try self.schedulePerformanceObserverDelivery();
|
||||
}
|
||||
|
||||
/// Schedules async delivery of performance observer records.
|
||||
pub fn schedulePerformanceObserverDelivery(self: *Page) !void {
|
||||
// Already scheduled.
|
||||
if (self._performance_delivery_scheduled) {
|
||||
return;
|
||||
@@ -1659,10 +1614,10 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
|
||||
.{ ._proto = undefined, ._tag_name = String.init(undefined, "dd", .{}) catch unreachable, ._tag = .dd },
|
||||
),
|
||||
asUint("dl") => return self.createHtmlElementT(
|
||||
Element.Html.Generic,
|
||||
Element.Html.DList,
|
||||
namespace,
|
||||
attribute_iterator,
|
||||
.{ ._proto = undefined, ._tag_name = String.init(undefined, "dl", .{}) catch unreachable, ._tag = .dl },
|
||||
.{ ._proto = undefined },
|
||||
),
|
||||
asUint("dt") => return self.createHtmlElementT(
|
||||
Element.Html.Generic,
|
||||
@@ -2911,6 +2866,7 @@ const ParseState = union(enum) {
|
||||
err: anyerror,
|
||||
html: std.ArrayList(u8),
|
||||
text: std.ArrayList(u8),
|
||||
image: std.ArrayList(u8),
|
||||
raw: std.ArrayList(u8),
|
||||
raw_done: []const u8,
|
||||
};
|
||||
@@ -3089,21 +3045,25 @@ pub fn handleClick(self: *Page, target: *Node) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
try element.focus(self);
|
||||
try self.scheduleNavigation(href, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .push = null },
|
||||
}, .anchor);
|
||||
},
|
||||
.input => |input| switch (input._input_type) {
|
||||
.submit => return self.submitForm(element, input.getForm(self), .{}),
|
||||
else => self.window._document._active_element = element,
|
||||
.input => |input| {
|
||||
try element.focus(self);
|
||||
if (input._input_type == .submit) {
|
||||
return self.submitForm(element, input.getForm(self), .{});
|
||||
}
|
||||
},
|
||||
.button => |button| {
|
||||
try element.focus(self);
|
||||
if (std.mem.eql(u8, button.getType(), "submit")) {
|
||||
return self.submitForm(element, button.getForm(self), .{});
|
||||
}
|
||||
},
|
||||
.select, .textarea => self.window._document._active_element = element,
|
||||
.select, .textarea => try element.focus(self),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,54 @@
|
||||
// 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 builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
const log = @import("../log.zig");
|
||||
|
||||
pub const CompiledPattern = struct {
|
||||
pattern: []const u8,
|
||||
ty: enum {
|
||||
prefix, // "/admin/" - prefix match
|
||||
exact, // "/admin$" - exact match
|
||||
wildcard, // any pattern that contains *
|
||||
},
|
||||
|
||||
fn compile(pattern: []const u8) CompiledPattern {
|
||||
if (pattern.len == 0) {
|
||||
return .{
|
||||
.pattern = pattern,
|
||||
.ty = .prefix,
|
||||
};
|
||||
}
|
||||
|
||||
const is_wildcard = std.mem.indexOfScalar(u8, pattern, '*') != null;
|
||||
|
||||
if (is_wildcard) {
|
||||
return .{
|
||||
.pattern = pattern,
|
||||
.ty = .wildcard,
|
||||
};
|
||||
}
|
||||
|
||||
const has_end_anchor = pattern[pattern.len - 1] == '$';
|
||||
return .{
|
||||
.pattern = pattern,
|
||||
.ty = if (has_end_anchor) .exact else .prefix,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Rule = union(enum) {
|
||||
allow: []const u8,
|
||||
disallow: []const u8,
|
||||
allow: CompiledPattern,
|
||||
disallow: CompiledPattern,
|
||||
|
||||
fn allowRule(pattern: []const u8) Rule {
|
||||
return .{ .allow = CompiledPattern.compile(pattern) };
|
||||
}
|
||||
|
||||
fn disallowRule(pattern: []const u8) Rule {
|
||||
return .{ .disallow = CompiledPattern.compile(pattern) };
|
||||
}
|
||||
};
|
||||
|
||||
pub const Key = enum {
|
||||
@@ -44,11 +86,22 @@ pub const RobotStore = struct {
|
||||
const Context = @This();
|
||||
|
||||
pub fn hash(_: Context, value: []const u8) u32 {
|
||||
var hasher = std.hash.Wyhash.init(value.len);
|
||||
for (value) |c| {
|
||||
std.hash.autoHash(&hasher, std.ascii.toLower(c));
|
||||
var key = value;
|
||||
var buf: [128]u8 = undefined;
|
||||
var h = std.hash.Wyhash.init(value.len);
|
||||
|
||||
while (key.len >= 128) {
|
||||
const lower = std.ascii.lowerString(buf[0..], key[0..128]);
|
||||
h.update(lower);
|
||||
key = key[128..];
|
||||
}
|
||||
return @truncate(hasher.final());
|
||||
|
||||
if (key.len > 0) {
|
||||
const lower = std.ascii.lowerString(buf[0..key.len], key);
|
||||
h.update(lower);
|
||||
}
|
||||
|
||||
return @truncate(h.final());
|
||||
}
|
||||
|
||||
pub fn eql(_: Context, a: []const u8, b: []const u8) bool {
|
||||
@@ -58,12 +111,16 @@ pub const RobotStore = struct {
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
map: RobotsMap,
|
||||
mutex: std.Thread.Mutex = .{},
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) RobotStore {
|
||||
return .{ .allocator = allocator, .map = .empty };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *RobotStore) void {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
var iter = self.map.iterator();
|
||||
|
||||
while (iter.next()) |entry| {
|
||||
@@ -79,6 +136,9 @@ pub const RobotStore = struct {
|
||||
}
|
||||
|
||||
pub fn get(self: *RobotStore, url: []const u8) ?RobotsEntry {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
return self.map.get(url);
|
||||
}
|
||||
|
||||
@@ -87,11 +147,17 @@ pub const RobotStore = struct {
|
||||
}
|
||||
|
||||
pub fn put(self: *RobotStore, url: []const u8, robots: Robots) !void {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
const duped = try self.allocator.dupe(u8, url);
|
||||
try self.map.put(self.allocator, duped, .{ .present = robots });
|
||||
}
|
||||
|
||||
pub fn putAbsent(self: *RobotStore, url: []const u8) !void {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
const duped = try self.allocator.dupe(u8, url);
|
||||
try self.map.put(self.allocator, duped, .absent);
|
||||
}
|
||||
@@ -112,8 +178,8 @@ const State = struct {
|
||||
fn freeRulesInList(allocator: std.mem.Allocator, rules: []const Rule) void {
|
||||
for (rules) |rule| {
|
||||
switch (rule) {
|
||||
.allow => |value| allocator.free(value),
|
||||
.disallow => |value| allocator.free(value),
|
||||
.allow => |compiled| allocator.free(compiled.pattern),
|
||||
.disallow => |compiled| allocator.free(compiled.pattern),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +188,7 @@ fn parseRulesWithUserAgent(
|
||||
allocator: std.mem.Allocator,
|
||||
user_agent: []const u8,
|
||||
raw_bytes: []const u8,
|
||||
) ![]const Rule {
|
||||
) ![]Rule {
|
||||
var rules: std.ArrayList(Rule) = .empty;
|
||||
defer rules.deinit(allocator);
|
||||
|
||||
@@ -201,13 +267,13 @@ fn parseRulesWithUserAgent(
|
||||
.in_our_entry => {
|
||||
const duped_value = try allocator.dupe(u8, value);
|
||||
errdefer allocator.free(duped_value);
|
||||
try rules.append(allocator, .{ .allow = duped_value });
|
||||
try rules.append(allocator, Rule.allowRule(duped_value));
|
||||
},
|
||||
.in_other_entry => {},
|
||||
.in_wildcard_entry => {
|
||||
const duped_value = try allocator.dupe(u8, value);
|
||||
errdefer allocator.free(duped_value);
|
||||
try wildcard_rules.append(allocator, .{ .allow = duped_value });
|
||||
try wildcard_rules.append(allocator, Rule.allowRule(duped_value));
|
||||
},
|
||||
.not_in_entry => {
|
||||
log.warn(.browser, "robots unexpected rule", .{ .rule = "allow" });
|
||||
@@ -220,15 +286,19 @@ fn parseRulesWithUserAgent(
|
||||
|
||||
switch (state.entry) {
|
||||
.in_our_entry => {
|
||||
if (value.len == 0) continue;
|
||||
|
||||
const duped_value = try allocator.dupe(u8, value);
|
||||
errdefer allocator.free(duped_value);
|
||||
try rules.append(allocator, .{ .disallow = duped_value });
|
||||
try rules.append(allocator, Rule.disallowRule(duped_value));
|
||||
},
|
||||
.in_other_entry => {},
|
||||
.in_wildcard_entry => {
|
||||
if (value.len == 0) continue;
|
||||
|
||||
const duped_value = try allocator.dupe(u8, value);
|
||||
errdefer allocator.free(duped_value);
|
||||
try wildcard_rules.append(allocator, .{ .disallow = duped_value });
|
||||
try wildcard_rules.append(allocator, Rule.disallowRule(duped_value));
|
||||
},
|
||||
.not_in_entry => {
|
||||
log.warn(.browser, "robots unexpected rule", .{ .rule = "disallow" });
|
||||
@@ -252,6 +322,39 @@ fn parseRulesWithUserAgent(
|
||||
|
||||
pub fn fromBytes(allocator: std.mem.Allocator, user_agent: []const u8, bytes: []const u8) !Robots {
|
||||
const rules = try parseRulesWithUserAgent(allocator, user_agent, bytes);
|
||||
|
||||
// sort by order once.
|
||||
std.mem.sort(Rule, rules, {}, struct {
|
||||
fn lessThan(_: void, a: Rule, b: Rule) bool {
|
||||
const a_len = switch (a) {
|
||||
.allow => |p| p.pattern.len,
|
||||
.disallow => |p| p.pattern.len,
|
||||
};
|
||||
|
||||
const b_len = switch (b) {
|
||||
.allow => |p| p.pattern.len,
|
||||
.disallow => |p| p.pattern.len,
|
||||
};
|
||||
|
||||
// Sort by length first.
|
||||
if (a_len != b_len) {
|
||||
return a_len > b_len;
|
||||
}
|
||||
|
||||
// Otherwise, allow should beat disallow.
|
||||
const a_is_allow = switch (a) {
|
||||
.allow => true,
|
||||
.disallow => false,
|
||||
};
|
||||
const b_is_allow = switch (b) {
|
||||
.allow => true,
|
||||
.disallow => false,
|
||||
};
|
||||
|
||||
return a_is_allow and !b_is_allow;
|
||||
}
|
||||
}.lessThan);
|
||||
|
||||
return .{ .rules = rules };
|
||||
}
|
||||
|
||||
@@ -260,86 +363,102 @@ pub fn deinit(self: *Robots, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.rules);
|
||||
}
|
||||
|
||||
fn matchPatternRecursive(pattern: []const u8, path: []const u8, exact_match: bool) bool {
|
||||
if (pattern.len == 0) return true;
|
||||
|
||||
const star_pos = std.mem.indexOfScalar(u8, pattern, '*') orelse {
|
||||
if (exact_match) {
|
||||
// If we end in '$', we must be exactly equal.
|
||||
return std.mem.eql(u8, path, pattern);
|
||||
} else {
|
||||
// Otherwise, we are just a prefix.
|
||||
return std.mem.startsWith(u8, path, pattern);
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure the prefix before the '*' matches.
|
||||
if (!std.mem.startsWith(u8, path, pattern[0..star_pos])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const suffix_pattern = pattern[star_pos + 1 ..];
|
||||
if (suffix_pattern.len == 0) return true;
|
||||
|
||||
var i: usize = star_pos;
|
||||
while (i <= path.len) : (i += 1) {
|
||||
if (matchPatternRecursive(suffix_pattern, path[i..], exact_match)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// There are rules for how the pattern in robots.txt should be matched.
|
||||
///
|
||||
/// * should match 0 or more of any character.
|
||||
/// $ should signify the end of a path, making it exact.
|
||||
/// otherwise, it is a prefix path.
|
||||
fn matchPattern(pattern: []const u8, path: []const u8) ?usize {
|
||||
if (pattern.len == 0) return 0;
|
||||
const exact_match = pattern[pattern.len - 1] == '$';
|
||||
const inner_pattern = if (exact_match) pattern[0 .. pattern.len - 1] else pattern;
|
||||
fn matchPattern(compiled: CompiledPattern, path: []const u8) bool {
|
||||
switch (compiled.ty) {
|
||||
.prefix => return std.mem.startsWith(u8, path, compiled.pattern),
|
||||
.exact => {
|
||||
const pattern = compiled.pattern;
|
||||
return std.mem.eql(u8, path, pattern[0 .. pattern.len - 1]);
|
||||
},
|
||||
.wildcard => {
|
||||
const pattern = compiled.pattern;
|
||||
const exact_match = pattern[pattern.len - 1] == '$';
|
||||
const inner_pattern = if (exact_match) pattern[0 .. pattern.len - 1] else pattern;
|
||||
return matchInnerPattern(inner_pattern, path, exact_match);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (matchPatternRecursive(
|
||||
inner_pattern,
|
||||
path,
|
||||
exact_match,
|
||||
)) return pattern.len else return null;
|
||||
fn matchInnerPattern(pattern: []const u8, path: []const u8, exact_match: bool) bool {
|
||||
var pattern_idx: usize = 0;
|
||||
var path_idx: usize = 0;
|
||||
|
||||
var star_pattern_idx: ?usize = null;
|
||||
var star_path_idx: ?usize = null;
|
||||
|
||||
while (pattern_idx < pattern.len or path_idx < path.len) {
|
||||
// 1: If pattern is consumed and we are doing prefix match, we matched.
|
||||
if (pattern_idx >= pattern.len and !exact_match) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2: Current character is a wildcard
|
||||
if (pattern_idx < pattern.len and pattern[pattern_idx] == '*') {
|
||||
star_pattern_idx = pattern_idx;
|
||||
star_path_idx = path_idx;
|
||||
pattern_idx += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3: Characters match, advance both heads.
|
||||
if (pattern_idx < pattern.len and path_idx < path.len and pattern[pattern_idx] == path[path_idx]) {
|
||||
pattern_idx += 1;
|
||||
path_idx += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4: we have a previous wildcard, backtrack and try matching more.
|
||||
if (star_pattern_idx) |star_p_idx| {
|
||||
// if we have exhausted the path,
|
||||
// we know we haven't matched.
|
||||
if (star_path_idx.? > path.len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pattern_idx = star_p_idx + 1;
|
||||
path_idx = star_path_idx.?;
|
||||
star_path_idx.? += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallthrough: No match and no backtracking.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle trailing widlcards that can match 0 characters.
|
||||
while (pattern_idx < pattern.len and pattern[pattern_idx] == '*') {
|
||||
pattern_idx += 1;
|
||||
}
|
||||
|
||||
if (exact_match) {
|
||||
// Both must be fully consumed.
|
||||
return pattern_idx == pattern.len and path_idx == path.len;
|
||||
}
|
||||
|
||||
// For prefix match, pattern must be completed.
|
||||
return pattern_idx == pattern.len;
|
||||
}
|
||||
|
||||
pub fn isAllowed(self: *const Robots, path: []const u8) bool {
|
||||
const rules = self.rules;
|
||||
|
||||
var longest_match_len: usize = 0;
|
||||
var is_allowed_result = true;
|
||||
|
||||
for (rules) |rule| {
|
||||
for (self.rules) |rule| {
|
||||
switch (rule) {
|
||||
.allow => |pattern| {
|
||||
if (matchPattern(pattern, path)) |len| {
|
||||
// Longest or Last Wins.
|
||||
if (len >= longest_match_len) {
|
||||
longest_match_len = len;
|
||||
is_allowed_result = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
.disallow => |pattern| {
|
||||
if (pattern.len == 0) continue;
|
||||
|
||||
if (matchPattern(pattern, path)) |len| {
|
||||
// Longest or Last Wins.
|
||||
if (len >= longest_match_len) {
|
||||
longest_match_len = len;
|
||||
is_allowed_result = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
.allow => |compiled| if (matchPattern(compiled, path)) return true,
|
||||
.disallow => |compiled| if (matchPattern(compiled, path)) return false,
|
||||
}
|
||||
}
|
||||
|
||||
return is_allowed_result;
|
||||
return true;
|
||||
}
|
||||
|
||||
fn testMatch(pattern: []const u8, path: []const u8) bool {
|
||||
comptime if (!builtin.is_test) unreachable;
|
||||
|
||||
return matchPattern(CompiledPattern.compile(pattern), path);
|
||||
}
|
||||
|
||||
test "Robots: simple robots.txt" {
|
||||
@@ -362,77 +481,77 @@ test "Robots: simple robots.txt" {
|
||||
}
|
||||
|
||||
try std.testing.expectEqual(1, rules.len);
|
||||
try std.testing.expectEqualStrings("/admin/", rules[0].disallow);
|
||||
try std.testing.expectEqualStrings("/admin/", rules[0].disallow.pattern);
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - simple prefix" {
|
||||
try std.testing.expect(matchPattern("/admin", "/admin/page") != null);
|
||||
try std.testing.expect(matchPattern("/admin", "/admin") != null);
|
||||
try std.testing.expect(matchPattern("/admin", "/other") == null);
|
||||
try std.testing.expect(matchPattern("/admin/page", "/admin") == null);
|
||||
try std.testing.expect(testMatch("/admin", "/admin/page"));
|
||||
try std.testing.expect(testMatch("/admin", "/admin"));
|
||||
try std.testing.expect(!testMatch("/admin", "/other"));
|
||||
try std.testing.expect(!testMatch("/admin/page", "/admin"));
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - single wildcard" {
|
||||
try std.testing.expect(matchPattern("/admin/*", "/admin/") != null);
|
||||
try std.testing.expect(matchPattern("/admin/*", "/admin/page") != null);
|
||||
try std.testing.expect(matchPattern("/admin/*", "/admin/page/subpage") != null);
|
||||
try std.testing.expect(matchPattern("/admin/*", "/other/page") == null);
|
||||
try std.testing.expect(testMatch("/admin/*", "/admin/"));
|
||||
try std.testing.expect(testMatch("/admin/*", "/admin/page"));
|
||||
try std.testing.expect(testMatch("/admin/*", "/admin/page/subpage"));
|
||||
try std.testing.expect(!testMatch("/admin/*", "/other/page"));
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - wildcard in middle" {
|
||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/abc/def/xyz") != null);
|
||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/abc/def/ghi/xyz") != null);
|
||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/abc/def") == null);
|
||||
try std.testing.expect(matchPattern("/abc/*/xyz", "/other/def/xyz") == null);
|
||||
try std.testing.expect(testMatch("/abc/*/xyz", "/abc/def/xyz"));
|
||||
try std.testing.expect(testMatch("/abc/*/xyz", "/abc/def/ghi/xyz"));
|
||||
try std.testing.expect(!testMatch("/abc/*/xyz", "/abc/def"));
|
||||
try std.testing.expect(!testMatch("/abc/*/xyz", "/other/def/xyz"));
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - complex wildcard case" {
|
||||
try std.testing.expect(matchPattern("/abc/*/def/xyz", "/abc/def/def/xyz") != null);
|
||||
try std.testing.expect(matchPattern("/abc/*/def/xyz", "/abc/ANYTHING/def/xyz") != null);
|
||||
try std.testing.expect(testMatch("/abc/*/def/xyz", "/abc/def/def/xyz"));
|
||||
try std.testing.expect(testMatch("/abc/*/def/xyz", "/abc/ANYTHING/def/xyz"));
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - multiple wildcards" {
|
||||
try std.testing.expect(matchPattern("/a/*/b/*/c", "/a/x/b/y/c") != null);
|
||||
try std.testing.expect(matchPattern("/a/*/b/*/c", "/a/x/y/b/z/w/c") != null);
|
||||
try std.testing.expect(matchPattern("/*.php", "/index.php") != null);
|
||||
try std.testing.expect(matchPattern("/*.php", "/admin/index.php") != null);
|
||||
try std.testing.expect(testMatch("/a/*/b/*/c", "/a/x/b/y/c"));
|
||||
try std.testing.expect(testMatch("/a/*/b/*/c", "/a/x/y/b/z/w/c"));
|
||||
try std.testing.expect(testMatch("/*.php", "/index.php"));
|
||||
try std.testing.expect(testMatch("/*.php", "/admin/index.php"));
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - end anchor" {
|
||||
try std.testing.expect(matchPattern("/*.php$", "/index.php") != null);
|
||||
try std.testing.expect(matchPattern("/*.php$", "/index.php?param=value") == null);
|
||||
try std.testing.expect(matchPattern("/admin$", "/admin") != null);
|
||||
try std.testing.expect(matchPattern("/admin$", "/admin/") == null);
|
||||
try std.testing.expect(matchPattern("/fish$", "/fish") != null);
|
||||
try std.testing.expect(matchPattern("/fish$", "/fishheads") == null);
|
||||
try std.testing.expect(testMatch("/*.php$", "/index.php"));
|
||||
try std.testing.expect(!testMatch("/*.php$", "/index.php?param=value"));
|
||||
try std.testing.expect(testMatch("/admin$", "/admin"));
|
||||
try std.testing.expect(!testMatch("/admin$", "/admin/"));
|
||||
try std.testing.expect(testMatch("/fish$", "/fish"));
|
||||
try std.testing.expect(!testMatch("/fish$", "/fishheads"));
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - wildcard with extension" {
|
||||
try std.testing.expect(matchPattern("/fish*.php", "/fish.php") != null);
|
||||
try std.testing.expect(matchPattern("/fish*.php", "/fishheads.php") != null);
|
||||
try std.testing.expect(matchPattern("/fish*.php", "/fish/salmon.php") != null);
|
||||
try std.testing.expect(matchPattern("/fish*.php", "/fish.asp") == null);
|
||||
try std.testing.expect(testMatch("/fish*.php", "/fish.php"));
|
||||
try std.testing.expect(testMatch("/fish*.php", "/fishheads.php"));
|
||||
try std.testing.expect(testMatch("/fish*.php", "/fish/salmon.php"));
|
||||
try std.testing.expect(!testMatch("/fish*.php", "/fish.asp"));
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - empty and edge cases" {
|
||||
try std.testing.expect(matchPattern("", "/anything") != null);
|
||||
try std.testing.expect(matchPattern("/", "/") != null);
|
||||
try std.testing.expect(matchPattern("*", "/anything") != null);
|
||||
try std.testing.expect(matchPattern("/*", "/anything") != null);
|
||||
try std.testing.expect(matchPattern("$", "") != null);
|
||||
try std.testing.expect(testMatch("", "/anything"));
|
||||
try std.testing.expect(testMatch("/", "/"));
|
||||
try std.testing.expect(testMatch("*", "/anything"));
|
||||
try std.testing.expect(testMatch("/*", "/anything"));
|
||||
try std.testing.expect(testMatch("$", ""));
|
||||
}
|
||||
|
||||
test "Robots: matchPattern - real world examples" {
|
||||
try std.testing.expect(matchPattern("/", "/anything") != null);
|
||||
try std.testing.expect(testMatch("/", "/anything"));
|
||||
|
||||
try std.testing.expect(matchPattern("/admin/", "/admin/page") != null);
|
||||
try std.testing.expect(matchPattern("/admin/", "/public/page") == null);
|
||||
try std.testing.expect(testMatch("/admin/", "/admin/page"));
|
||||
try std.testing.expect(!testMatch("/admin/", "/public/page"));
|
||||
|
||||
try std.testing.expect(matchPattern("/*.pdf$", "/document.pdf") != null);
|
||||
try std.testing.expect(matchPattern("/*.pdf$", "/document.pdf.bak") == null);
|
||||
try std.testing.expect(testMatch("/*.pdf$", "/document.pdf"));
|
||||
try std.testing.expect(!testMatch("/*.pdf$", "/document.pdf.bak"));
|
||||
|
||||
try std.testing.expect(matchPattern("/*?", "/page?param=value") != null);
|
||||
try std.testing.expect(matchPattern("/*?", "/page") == null);
|
||||
try std.testing.expect(testMatch("/*?", "/page?param=value"));
|
||||
try std.testing.expect(!testMatch("/*?", "/page"));
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - basic allow/disallow" {
|
||||
@@ -675,7 +794,7 @@ test "Robots: isAllowed - complex real-world example" {
|
||||
try std.testing.expect(robots.isAllowed("/cgi-bin/script.sh") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - order doesn't matter for same length" {
|
||||
test "Robots: isAllowed - order doesn't matter + allow wins" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var robots = try Robots.fromBytes(allocator, "Bot",
|
||||
@@ -687,7 +806,7 @@ test "Robots: isAllowed - order doesn't matter for same length" {
|
||||
);
|
||||
defer robots.deinit(allocator);
|
||||
|
||||
try std.testing.expect(robots.isAllowed("/page") == false);
|
||||
try std.testing.expect(robots.isAllowed("/page") == true);
|
||||
}
|
||||
|
||||
test "Robots: isAllowed - empty file uses wildcard defaults" {
|
||||
|
||||
@@ -83,10 +83,7 @@ imported_modules: std.StringHashMapUnmanaged(ImportedModule),
|
||||
// importmap contains resolved urls.
|
||||
importmap: std.StringHashMapUnmanaged([:0]const u8),
|
||||
|
||||
pub fn init(page: *Page) ScriptManager {
|
||||
// page isn't fully initialized, we can setup our reference, but that's it.
|
||||
const browser = page._session.browser;
|
||||
const allocator = browser.allocator;
|
||||
pub fn init(allocator: Allocator, http_client: *Http.Client, page: *Page) ScriptManager {
|
||||
return .{
|
||||
.page = page,
|
||||
.async_scripts = .{},
|
||||
@@ -96,7 +93,7 @@ pub fn init(page: *Page) ScriptManager {
|
||||
.is_evaluating = false,
|
||||
.allocator = allocator,
|
||||
.imported_modules = .empty,
|
||||
.client = browser.http_client,
|
||||
.client = http_client,
|
||||
.static_scripts_done = false,
|
||||
.buffer_pool = BufferPool.init(allocator, 5),
|
||||
.script_pool = std.heap.MemoryPool(Script).init(allocator),
|
||||
@@ -620,6 +617,7 @@ pub const Script = struct {
|
||||
node: std.DoublyLinkedList.Node,
|
||||
script_element: ?*Element.Html.Script,
|
||||
manager: *ScriptManager,
|
||||
header_callback_called: bool = false,
|
||||
|
||||
const Kind = enum {
|
||||
module,
|
||||
@@ -684,7 +682,15 @@ pub const Script = struct {
|
||||
});
|
||||
}
|
||||
|
||||
lp.assert(self.source.remote.capacity == 0, "ScriptManager.HeaderCallback", .{ .capacity = self.source.remote.capacity });
|
||||
{
|
||||
// temp debug, trying to figure out why the next assert sometimes
|
||||
// fails. Is the buffer just corrupt or is headerCallback really
|
||||
// being called twice?
|
||||
lp.assert(self.header_callback_called == false, "ScriptManager.Header recall", .{});
|
||||
self.header_callback_called = true;
|
||||
}
|
||||
|
||||
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
||||
var buffer = self.manager.buffer_pool.get();
|
||||
if (transfer.getContentLength()) |cl| {
|
||||
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
|
||||
@@ -897,7 +903,7 @@ const BufferPool = struct {
|
||||
max_concurrent_transfers: u8,
|
||||
mem_pool: std.heap.MemoryPool(Container),
|
||||
|
||||
const List = std.DoublyLinkedList;
|
||||
const List = std.SinglyLinkedList;
|
||||
|
||||
const Container = struct {
|
||||
node: List.Node,
|
||||
@@ -956,7 +962,7 @@ const BufferPool = struct {
|
||||
b.clearRetainingCapacity();
|
||||
container.* = .{ .buf = b, .node = .{} };
|
||||
self.count += 1;
|
||||
self.available.append(&container.node);
|
||||
self.available.prepend(&container.node);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -65,18 +65,23 @@ page: ?Page,
|
||||
|
||||
pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void {
|
||||
const allocator = browser.app.allocator;
|
||||
const session_allocator = browser.session_arena.allocator();
|
||||
const arena = try browser.arena_pool.acquire();
|
||||
errdefer browser.arena_pool.release(arena);
|
||||
|
||||
const transfer_arena = try browser.arena_pool.acquire();
|
||||
errdefer browser.arena_pool.release(transfer_arena);
|
||||
|
||||
self.* = .{
|
||||
.page = null,
|
||||
.arena = arena,
|
||||
.history = .{},
|
||||
.navigation = .{},
|
||||
// The prototype (EventTarget) for Navigation is created when a Page is created.
|
||||
.navigation = .{ ._proto = undefined },
|
||||
.storage_shed = .{},
|
||||
.browser = browser,
|
||||
.notification = notification,
|
||||
.arena = session_allocator,
|
||||
.transfer_arena = transfer_arena,
|
||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||
.transfer_arena = browser.transfer_arena.allocator(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,8 +89,12 @@ pub fn deinit(self: *Session) void {
|
||||
if (self.page != null) {
|
||||
self.removePage();
|
||||
}
|
||||
const browser = self.browser;
|
||||
|
||||
self.cookie_jar.deinit();
|
||||
self.storage_shed.deinit(self.browser.app.allocator);
|
||||
self.storage_shed.deinit(browser.app.allocator);
|
||||
browser.arena_pool.release(self.transfer_arena);
|
||||
browser.arena_pool.release(self.arena);
|
||||
}
|
||||
|
||||
// NOTE: the caller is not the owner of the returned value,
|
||||
@@ -93,8 +102,6 @@ pub fn deinit(self: *Session) void {
|
||||
pub fn createPage(self: *Session) !*Page {
|
||||
lp.assert(self.page == null, "Session.createPage - page not null", .{});
|
||||
|
||||
_ = self.browser.page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
|
||||
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, self);
|
||||
@@ -127,6 +134,21 @@ pub fn removePage(self: *Session) void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replacePage(self: *Session) !*Page {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.browser, "replace page", .{});
|
||||
}
|
||||
|
||||
lp.assert(self.page != null, "Session.replacePage null page", .{});
|
||||
self.page.?.deinit();
|
||||
self.browser.env.memoryPressureNotification(.moderate);
|
||||
|
||||
self.page = @as(Page, undefined);
|
||||
const page = &self.page.?;
|
||||
try Page.init(page, self);
|
||||
return page;
|
||||
}
|
||||
|
||||
pub fn currentPage(self: *Session) ?*Page {
|
||||
return &(self.page orelse return null);
|
||||
}
|
||||
@@ -158,7 +180,7 @@ pub fn wait(self: *Session, wait_ms: u32) WaitResult {
|
||||
}
|
||||
|
||||
fn processScheduledNavigation(self: *Session) !void {
|
||||
defer _ = self.browser.transfer_arena.reset(.{ .retain_with_limit = 8 * 1024 });
|
||||
defer self.browser.arena_pool.reset(self.transfer_arena, 4 * 1024);
|
||||
const url, const opts = blk: {
|
||||
const qn = self.page.?._queued_navigation.?;
|
||||
// qn might not be safe to use after self.removePage is called, hence
|
||||
|
||||
@@ -23,6 +23,7 @@ const string = @import("../../string.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const js = @import("js.zig");
|
||||
const Local = @import("Local.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const TaggedOpaque = @import("TaggedOpaque.zig");
|
||||
|
||||
@@ -33,25 +34,24 @@ const CALL_ARENA_RETAIN = 1024 * 16;
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Caller = @This();
|
||||
local: js.Local,
|
||||
local: Local,
|
||||
prev_local: ?*const js.Local,
|
||||
prev_context: *Context,
|
||||
|
||||
// Takes the raw v8 isolate and extracts the context from it.
|
||||
pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
|
||||
const v8_context_handle = v8.v8__Isolate__GetCurrentContext(v8_isolate);
|
||||
|
||||
const embedder_data = v8.v8__Context__GetEmbedderData(v8_context_handle, 1);
|
||||
var lossless: bool = undefined;
|
||||
const ctx: *Context = @ptrFromInt(v8.v8__BigInt__Uint64Value(embedder_data, &lossless));
|
||||
const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate).?;
|
||||
initWithContext(self, Context.fromC(v8_context), v8_context);
|
||||
}
|
||||
|
||||
fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) void {
|
||||
ctx.call_depth += 1;
|
||||
self.* = Caller{
|
||||
.local = .{
|
||||
.ctx = ctx,
|
||||
.handle = v8_context_handle.?,
|
||||
.handle = v8_context,
|
||||
.call_arena = ctx.call_arena,
|
||||
.isolate = .{ .handle = v8_isolate },
|
||||
.isolate = ctx.isolate,
|
||||
},
|
||||
.prev_local = ctx.local,
|
||||
.prev_context = ctx.page.js,
|
||||
@@ -92,25 +92,28 @@ pub const CallOpts = struct {
|
||||
};
|
||||
|
||||
pub fn constructor(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = FunctionCallbackInfo{ .handle = handle };
|
||||
|
||||
if (!info.isConstructCall()) {
|
||||
self.handleError(T, @TypeOf(func), error.InvalidArgument, info, opts);
|
||||
handleError(T, @TypeOf(func), local, error.InvalidArgument, info, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
self._constructor(func, info) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||
};
|
||||
}
|
||||
|
||||
fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void {
|
||||
const F = @TypeOf(func);
|
||||
const args = try self.getArgs(F, 0, info);
|
||||
const local = &self.local;
|
||||
const args = try getArgs(F, 0, local, info);
|
||||
const res = @call(.auto, func, args);
|
||||
|
||||
const ReturnType = @typeInfo(F).@"fn".return_type orelse {
|
||||
@@ -118,12 +121,12 @@ fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void
|
||||
};
|
||||
|
||||
const new_this_handle = info.getThis();
|
||||
var this = js.Object{ .local = &self.local, .handle = new_this_handle };
|
||||
var this = js.Object{ .local = local, .handle = new_this_handle };
|
||||
if (@typeInfo(ReturnType) == .error_union) {
|
||||
const non_error_res = res catch |err| return err;
|
||||
this = try self.local.mapZigInstanceToJs(new_this_handle, non_error_res);
|
||||
this = try local.mapZigInstanceToJs(new_this_handle, non_error_res);
|
||||
} else {
|
||||
this = try self.local.mapZigInstanceToJs(new_this_handle, res);
|
||||
this = try local.mapZigInstanceToJs(new_this_handle, res);
|
||||
}
|
||||
|
||||
// If we got back a different object (existing wrapper), copy the prototype
|
||||
@@ -140,144 +143,115 @@ fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void
|
||||
info.getReturnValue().set(this.handle);
|
||||
}
|
||||
|
||||
pub fn method(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = FunctionCallbackInfo{ .handle = handle };
|
||||
self._method(T, func, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
};
|
||||
}
|
||||
|
||||
fn _method(self: *Caller, comptime T: type, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
|
||||
const F = @TypeOf(func);
|
||||
var args = try self.getArgs(F, 1, info);
|
||||
|
||||
const js_this = info.getThis();
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, js_this);
|
||||
|
||||
const res = @call(.auto, func, args);
|
||||
|
||||
const mapped = try self.local.zigValueToJs(res, opts);
|
||||
const return_value = info.getReturnValue();
|
||||
return_value.set(mapped);
|
||||
}
|
||||
|
||||
pub fn function(self: *Caller, comptime T: type, func: anytype, handle: *const v8.FunctionCallbackInfo, comptime opts: CallOpts) void {
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = FunctionCallbackInfo{ .handle = handle };
|
||||
self._function(func, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
};
|
||||
}
|
||||
|
||||
fn _function(self: *Caller, func: anytype, info: FunctionCallbackInfo, comptime opts: CallOpts) !void {
|
||||
const F = @TypeOf(func);
|
||||
const args = try self.getArgs(F, 0, info);
|
||||
const res = @call(.auto, func, args);
|
||||
info.getReturnValue().set(try self.local.zigValueToJs(res, opts));
|
||||
}
|
||||
|
||||
pub fn getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return self._getIndex(T, func, idx, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
return _getIndex(T, local, func, idx, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
fn _getIndex(comptime T: type, local: *const Local, func: anytype, idx: u32, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
const F = @TypeOf(func);
|
||||
var args = try self.getArgs(F, 2, info);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@field(args, "1") = idx;
|
||||
if (@typeInfo(F).@"fn".params.len == 3) {
|
||||
@field(args, "2") = local.ctx.page;
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return self.handleIndexedReturn(T, F, true, ret, info, opts);
|
||||
return handleIndexedReturn(T, F, true, local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return self._getNamedIndex(T, func, name, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
return _getNamedIndex(T, local, func, name, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
fn _getNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
const F = @TypeOf(func);
|
||||
var args = try self.getArgs(F, 2, info);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
|
||||
@field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name);
|
||||
if (@typeInfo(F).@"fn".params.len == 3) {
|
||||
@field(args, "2") = local.ctx.page;
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return self.handleIndexedReturn(T, F, true, ret, info, opts);
|
||||
return handleIndexedReturn(T, F, true, local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: *const v8.Value, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return self._setNamedIndex(T, func, name, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
return _setNamedIndex(T, local, func, name, .{ .local = &self.local, .handle = js_value }, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
fn _setNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, js_value: js.Value, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
|
||||
@field(args, "2") = try self.local.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
|
||||
@field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name);
|
||||
@field(args, "2") = try local.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
|
||||
if (@typeInfo(F).@"fn".params.len == 4) {
|
||||
@field(args, "3") = self.local.ctx.page;
|
||||
@field(args, "3") = local.ctx.page;
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return self.handleIndexedReturn(T, F, false, ret, info, opts);
|
||||
return handleIndexedReturn(T, F, false, local, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, handle: *const v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
const local = &self.local;
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.init(self.local.isolate);
|
||||
hs.init(local.isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
const info = PropertyCallbackInfo{ .handle = handle };
|
||||
return self._deleteNamedIndex(T, func, name, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
return _deleteNamedIndex(T, local, func, name, info, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), local, err, info, opts);
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
fn _deleteNamedIndex(comptime T: type, local: *const Local, func: anytype, name: *const v8.Name, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
@field(args, "1") = try self.nameToString(@TypeOf(args.@"1"), name);
|
||||
@field(args, "1") = try nameToString(local, @TypeOf(args.@"1"), name);
|
||||
if (@typeInfo(F).@"fn".params.len == 3) {
|
||||
@field(args, "2") = self.local.ctx.page;
|
||||
@field(args, "2") = local.ctx.page;
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return self.handleIndexedReturn(T, F, false, ret, info, opts);
|
||||
return handleIndexedReturn(T, F, false, local, ret, info, opts);
|
||||
}
|
||||
|
||||
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
fn handleIndexedReturn(comptime T: type, comptime F: type, comptime getter: bool, local: *const Local, ret: anytype, info: PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
// need to unwrap this error immediately for when opts.null_as_undefined == true
|
||||
// and we need to compare it to null;
|
||||
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
|
||||
@@ -292,7 +266,7 @@ fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, compti
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
self.handleError(T, F, err, info, opts);
|
||||
handleError(T, F, local, err, info, opts);
|
||||
// not intercepted
|
||||
return 0;
|
||||
};
|
||||
@@ -301,7 +275,7 @@ fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, compti
|
||||
};
|
||||
|
||||
if (comptime getter) {
|
||||
info.getReturnValue().set(try self.local.zigValueToJs(non_error_ret, opts));
|
||||
info.getReturnValue().set(try local.zigValueToJs(non_error_ret, opts));
|
||||
}
|
||||
// intercepted
|
||||
return 1;
|
||||
@@ -314,23 +288,23 @@ fn isInErrorSet(err: anyerror, comptime T: type) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn nameToString(self: *const Caller, comptime T: type, name: *const v8.Name) !T {
|
||||
fn nameToString(local: *const Local, comptime T: type, name: *const v8.Name) !T {
|
||||
const handle = @as(*const v8.String, @ptrCast(name));
|
||||
if (T == string.String) {
|
||||
return js.String.toSSO(.{ .local = &self.local, .handle = handle }, false);
|
||||
return js.String.toSSO(.{ .local = local, .handle = handle }, false);
|
||||
}
|
||||
if (T == string.Global) {
|
||||
return js.String.toSSO(.{ .local = &self.local, .handle = handle }, true);
|
||||
return js.String.toSSO(.{ .local = local, .handle = handle }, true);
|
||||
}
|
||||
return try js.String.toSlice(.{ .local = &self.local, .handle = handle });
|
||||
return try js.String.toSlice(.{ .local = local, .handle = handle });
|
||||
}
|
||||
|
||||
fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror, info: anytype, comptime opts: CallOpts) void {
|
||||
const isolate = self.local.isolate;
|
||||
fn handleError(comptime T: type, comptime F: type, local: *const Local, err: anyerror, info: anytype, comptime opts: CallOpts) void {
|
||||
const isolate = local.isolate;
|
||||
|
||||
if (comptime @import("builtin").mode == .Debug and @TypeOf(info) == FunctionCallbackInfo) {
|
||||
if (log.enabled(.js, .warn)) {
|
||||
self.logFunctionCallError(@typeName(T), @typeName(F), err, info);
|
||||
logFunctionCallError(local, @typeName(T), @typeName(F), err, info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +317,7 @@ fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror,
|
||||
if (comptime opts.dom_exception) {
|
||||
const DOMException = @import("../webapi/DOMException.zig");
|
||||
if (DOMException.fromError(err)) |ex| {
|
||||
const value = self.local.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error");
|
||||
const value = local.zigValueToJs(ex, .{}) catch break :blk isolate.createError("internal error");
|
||||
break :blk value.handle;
|
||||
}
|
||||
}
|
||||
@@ -355,120 +329,20 @@ fn handleError(self: *Caller, comptime T: type, comptime F: type, err: anyerror,
|
||||
info.getReturnValue().setValueHandle(js_exception);
|
||||
}
|
||||
|
||||
// If we call a method in javascript: cat.lives('nine');
|
||||
//
|
||||
// Then we'd expect a Zig function with 2 parameters: a self and the string.
|
||||
// In this case, offset == 1. Offset is always 1 for setters or methods.
|
||||
//
|
||||
// Offset is always 0 for constructors.
|
||||
//
|
||||
// For constructors, setters and methods, we can further increase offset + 1
|
||||
// if the first parameter is an instance of Page.
|
||||
//
|
||||
// Finally, if the JS function is called with _more_ parameters and
|
||||
// the last parameter in Zig is an array, we'll try to slurp the additional
|
||||
// parameters into the array.
|
||||
fn getArgs(self: *const Caller, comptime F: type, comptime offset: usize, info: anytype) !ParameterTypes(F) {
|
||||
const local = &self.local;
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
|
||||
const params = @typeInfo(F).@"fn".params[offset..];
|
||||
// Except for the constructor, the first parameter is always `self`
|
||||
// This isn't something we'll bind from JS, so skip it.
|
||||
const params_to_map = blk: {
|
||||
if (params.len == 0) {
|
||||
return args;
|
||||
}
|
||||
|
||||
// If the last parameter is the Page, set it, and exclude it
|
||||
// from our params slice, because we don't want to bind it to
|
||||
// a JS argument
|
||||
if (comptime isPage(params[params.len - 1].type.?)) {
|
||||
@field(args, tupleFieldName(params.len - 1 + offset)) = local.ctx.page;
|
||||
break :blk params[0 .. params.len - 1];
|
||||
}
|
||||
|
||||
// we have neither a Page nor a JsObject. All params must be
|
||||
// bound to a JavaScript value.
|
||||
break :blk params;
|
||||
};
|
||||
|
||||
if (params_to_map.len == 0) {
|
||||
return args;
|
||||
}
|
||||
|
||||
const js_parameter_count = info.length();
|
||||
const last_js_parameter = params_to_map.len - 1;
|
||||
var is_variadic = false;
|
||||
|
||||
{
|
||||
// This is going to get complicated. If the last Zig parameter
|
||||
// is a slice AND the corresponding javascript parameter is
|
||||
// NOT an an array, then we'll treat it as a variadic.
|
||||
|
||||
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
|
||||
const last_parameter_type_info = @typeInfo(last_parameter_type);
|
||||
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
|
||||
const slice_type = last_parameter_type_info.pointer.child;
|
||||
const corresponding_js_value = info.getArg(@intCast(last_js_parameter), local);
|
||||
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
|
||||
is_variadic = true;
|
||||
if (js_parameter_count == 0) {
|
||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||
} else if (js_parameter_count >= params_to_map.len) {
|
||||
const arr = try local.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
|
||||
for (arr, last_js_parameter..) |*a, i| {
|
||||
a.* = try local.jsValueToZig(slice_type, info.getArg(@intCast(i), local));
|
||||
}
|
||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
|
||||
} else {
|
||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline for (params_to_map, 0..) |param, i| {
|
||||
const field_index = comptime i + offset;
|
||||
if (comptime i == params_to_map.len - 1) {
|
||||
if (is_variadic) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (comptime isPage(param.type.?)) {
|
||||
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
|
||||
} else if (i >= js_parameter_count) {
|
||||
if (@typeInfo(param.type.?) != .optional) {
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
@field(args, tupleFieldName(field_index)) = null;
|
||||
} else {
|
||||
const js_val = info.getArg(@intCast(i), local);
|
||||
@field(args, tupleFieldName(field_index)) = local.jsValueToZig(param.type.?, js_val) catch {
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
// This is extracted to speed up compilation. When left inlined in handleError,
|
||||
// this can add as much as 10 seconds of compilation time.
|
||||
fn logFunctionCallError(self: *Caller, type_name: []const u8, func: []const u8, err: anyerror, info: FunctionCallbackInfo) void {
|
||||
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
|
||||
fn logFunctionCallError(local: *const Local, type_name: []const u8, func: []const u8, err: anyerror, info: FunctionCallbackInfo) void {
|
||||
const args_dump = serializeFunctionArgs(local, info) catch "failed to serialize args";
|
||||
log.info(.js, "function call error", .{
|
||||
.type = type_name,
|
||||
.func = func,
|
||||
.err = err,
|
||||
.args = args_dump,
|
||||
.stack = self.local.stackTrace() catch |err1| @errorName(err1),
|
||||
.stack = local.stackTrace() catch |err1| @errorName(err1),
|
||||
});
|
||||
}
|
||||
|
||||
fn serializeFunctionArgs(self: *Caller, info: FunctionCallbackInfo) ![]const u8 {
|
||||
const local = &self.local;
|
||||
fn serializeFunctionArgs(local: *const Local, info: FunctionCallbackInfo) ![]const u8 {
|
||||
var buf = std.Io.Writer.Allocating.init(local.call_arena);
|
||||
|
||||
const separator = log.separator();
|
||||
@@ -585,3 +459,274 @@ const ReturnValue = struct {
|
||||
v8.v8__ReturnValue__Set(self.handle, handle);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Function = struct {
|
||||
pub const Opts = struct {
|
||||
static: bool = false,
|
||||
dom_exception: bool = false,
|
||||
as_typed_array: bool = false,
|
||||
null_as_undefined: bool = false,
|
||||
cache: ?Caching = null,
|
||||
|
||||
// We support two ways to cache a value directly into a v8::Object. The
|
||||
// difference between the two is like the difference between a Map
|
||||
// and a Struct.
|
||||
// 1 - Using the object's internal fields. Think of this as
|
||||
// adding a field to the struct. It's fast, but the space is reserved
|
||||
// upfront for _every_ instance, whether we use it or not.
|
||||
//
|
||||
// 2 - Using the object's private state with a v8::Private key. Think of
|
||||
// this as a HashMap. It takes no memory if the cache isn't used
|
||||
// but has overhead when used.
|
||||
//
|
||||
// Consider `window.document`, (1) we have relatively few Window objects,
|
||||
// (2) They all have a document and (3) The document is accessed _a lot_.
|
||||
// An internal field makes sense.
|
||||
//
|
||||
// Consider `node.childNodes`, (1) we can have 20K+ node objects, (2)
|
||||
// 95% of nodes will never have their .childNodes access by JavaScript.
|
||||
// Private map lookup makes sense.
|
||||
pub const Caching = union(enum) {
|
||||
internal: u8,
|
||||
private: []const u8,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn call(comptime T: type, info_handle: *const v8.FunctionCallbackInfo, func: anytype, comptime opts: Opts) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(info_handle).?;
|
||||
const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate).?;
|
||||
|
||||
const ctx = Context.fromC(v8_context);
|
||||
const info = FunctionCallbackInfo{ .handle = info_handle };
|
||||
|
||||
var hs: js.HandleScope = undefined;
|
||||
hs.initWithIsolateHandle(v8_isolate);
|
||||
defer hs.deinit();
|
||||
|
||||
var cache_state: CacheState = undefined;
|
||||
if (comptime opts.cache) |cache| {
|
||||
// This API is a bit weird. On
|
||||
if (respondFromCache(cache, ctx, v8_context, info, &cache_state)) {
|
||||
// Value was fetched from the cache and returned already
|
||||
return;
|
||||
} else {
|
||||
// Cache miss: cache_state will have been populated
|
||||
}
|
||||
}
|
||||
|
||||
var caller: Caller = undefined;
|
||||
caller.initWithContext(ctx, v8_context);
|
||||
defer caller.deinit();
|
||||
|
||||
const js_value = _call(T, &caller.local, info, func, opts) catch |err| {
|
||||
handleError(T, @TypeOf(func), &caller.local, err, info, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
if (comptime opts.cache) |cache| {
|
||||
cache_state.save(cache, js_value);
|
||||
}
|
||||
}
|
||||
|
||||
fn _call(comptime T: type, local: *const Local, info: FunctionCallbackInfo, func: anytype, comptime opts: Opts) !js.Value {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
if (comptime opts.static) {
|
||||
args = try getArgs(F, 0, local, info);
|
||||
} else {
|
||||
args = try getArgs(F, 1, local, info);
|
||||
@field(args, "0") = try TaggedOpaque.fromJS(*T, info.getThis());
|
||||
}
|
||||
const res = @call(.auto, func, args);
|
||||
const js_value = try local.zigValueToJs(res, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
info.getReturnValue().set(js_value);
|
||||
return js_value;
|
||||
}
|
||||
|
||||
// We can cache a value directly into the v8::Object so that our callback to fetch a property
|
||||
// can be fast. Generally, think of it like this:
|
||||
// fn callback(handle: *const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
// const js_obj = info.getThis();
|
||||
// const cached_value = js_obj.getFromCache("Nodes.childNodes");
|
||||
// info.returnValue().set(cached_value);
|
||||
// }
|
||||
//
|
||||
// That above pseudocode snippet is largely what this respondFromCache is doing.
|
||||
// But on miss, it's also setting the `cache_state` with all of the data it
|
||||
// got checking the cache, so that, once we get the value from our Zig code,
|
||||
// it's quick to store in the v8::Object for subsequent calls.
|
||||
fn respondFromCache(comptime cache: Opts.Caching, ctx: *Context, v8_context: *const v8.Context, info: FunctionCallbackInfo, cache_state: *CacheState) bool {
|
||||
const js_this = info.getThis();
|
||||
const return_value = info.getReturnValue();
|
||||
|
||||
switch (cache) {
|
||||
.internal => |idx| {
|
||||
if (v8.v8__Object__GetInternalField(js_this, idx)) |cached| {
|
||||
// means we can't cache undefined, since we can't tell the
|
||||
// difference between "it isn't in the cache" and "it's
|
||||
// in the cache with a valud of undefined"
|
||||
if (!v8.v8__Value__IsUndefined(cached)) {
|
||||
return_value.set(cached);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// store this so that we can quickly save the result into the cache
|
||||
cache_state.* = .{
|
||||
.js_this = js_this,
|
||||
.v8_context = v8_context,
|
||||
.mode = .{ .internal = idx },
|
||||
};
|
||||
},
|
||||
.private => |private_symbol| {
|
||||
const global_handle = &@field(ctx.env.private_symbols, private_symbol).handle;
|
||||
const private_key: *const v8.Private = v8.v8__Global__Get(global_handle, ctx.isolate.handle).?;
|
||||
if (v8.v8__Object__GetPrivate(js_this, v8_context, private_key)) |cached| {
|
||||
// This means we can't cache "undefined", since we can't tell
|
||||
// the difference between a (a) undefined == not in the cache
|
||||
// and (b) undefined == the cache value. If this becomes
|
||||
// important, we can check HasPrivate first. But that requires
|
||||
// calling HasPrivate then GetPrivate.
|
||||
if (!v8.v8__Value__IsUndefined(cached)) {
|
||||
return_value.set(cached);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// store this so that we can quickly save the result into the cache
|
||||
cache_state.* = .{
|
||||
.js_this = js_this,
|
||||
.v8_context = v8_context,
|
||||
.mode = .{ .private = private_key },
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
// cache miss
|
||||
return false;
|
||||
}
|
||||
|
||||
const CacheState = struct {
|
||||
js_this: *const v8.Object,
|
||||
v8_context: *const v8.Context,
|
||||
mode: union(enum) {
|
||||
internal: u8,
|
||||
private: *const v8.Private,
|
||||
},
|
||||
|
||||
pub fn save(self: *const CacheState, comptime cache: Opts.Caching, js_value: js.Value) void {
|
||||
if (comptime cache == .internal) {
|
||||
v8.v8__Object__SetInternalField(self.js_this, self.mode.internal, js_value.handle);
|
||||
} else {
|
||||
var out: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__SetPrivate(self.js_this, self.v8_context, self.mode.private, js_value.handle, &out);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// If we call a method in javascript: cat.lives('nine');
|
||||
//
|
||||
// Then we'd expect a Zig function with 2 parameters: a self and the string.
|
||||
// In this case, offset == 1. Offset is always 1 for setters or methods.
|
||||
//
|
||||
// Offset is always 0 for constructors.
|
||||
//
|
||||
// For constructors, setters and methods, we can further increase offset + 1
|
||||
// if the first parameter is an instance of Page.
|
||||
//
|
||||
// Finally, if the JS function is called with _more_ parameters and
|
||||
// the last parameter in Zig is an array, we'll try to slurp the additional
|
||||
// parameters into the array.
|
||||
fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info: FunctionCallbackInfo) !ParameterTypes(F) {
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
|
||||
const params = @typeInfo(F).@"fn".params[offset..];
|
||||
// Except for the constructor, the first parameter is always `self`
|
||||
// This isn't something we'll bind from JS, so skip it.
|
||||
const params_to_map = blk: {
|
||||
if (params.len == 0) {
|
||||
return args;
|
||||
}
|
||||
|
||||
// If the last parameter is the Page, set it, and exclude it
|
||||
// from our params slice, because we don't want to bind it to
|
||||
// a JS argument
|
||||
if (comptime isPage(params[params.len - 1].type.?)) {
|
||||
@field(args, tupleFieldName(params.len - 1 + offset)) = local.ctx.page;
|
||||
break :blk params[0 .. params.len - 1];
|
||||
}
|
||||
|
||||
// we have neither a Page nor a JsObject. All params must be
|
||||
// bound to a JavaScript value.
|
||||
break :blk params;
|
||||
};
|
||||
|
||||
if (params_to_map.len == 0) {
|
||||
return args;
|
||||
}
|
||||
|
||||
const js_parameter_count = info.length();
|
||||
const last_js_parameter = params_to_map.len - 1;
|
||||
var is_variadic = false;
|
||||
|
||||
{
|
||||
// This is going to get complicated. If the last Zig parameter
|
||||
// is a slice AND the corresponding javascript parameter is
|
||||
// NOT an an array, then we'll treat it as a variadic.
|
||||
|
||||
const last_parameter_type = params_to_map[params_to_map.len - 1].type.?;
|
||||
const last_parameter_type_info = @typeInfo(last_parameter_type);
|
||||
if (last_parameter_type_info == .pointer and last_parameter_type_info.pointer.size == .slice) {
|
||||
const slice_type = last_parameter_type_info.pointer.child;
|
||||
const corresponding_js_value = info.getArg(@intCast(last_js_parameter), local);
|
||||
if (corresponding_js_value.isArray() == false and corresponding_js_value.isTypedArray() == false and slice_type != u8) {
|
||||
is_variadic = true;
|
||||
if (js_parameter_count == 0) {
|
||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||
} else if (js_parameter_count >= params_to_map.len) {
|
||||
const arr = try local.call_arena.alloc(last_parameter_type_info.pointer.child, js_parameter_count - params_to_map.len + 1);
|
||||
for (arr, last_js_parameter..) |*a, i| {
|
||||
a.* = try local.jsValueToZig(slice_type, info.getArg(@intCast(i), local));
|
||||
}
|
||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = arr;
|
||||
} else {
|
||||
@field(args, tupleFieldName(params_to_map.len + offset - 1)) = &.{};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline for (params_to_map, 0..) |param, i| {
|
||||
const field_index = comptime i + offset;
|
||||
if (comptime i == params_to_map.len - 1) {
|
||||
if (is_variadic) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (comptime isPage(param.type.?)) {
|
||||
@compileError("Page must be the last parameter (or 2nd last if there's a JsThis): " ++ @typeName(F));
|
||||
} else if (i >= js_parameter_count) {
|
||||
if (@typeInfo(param.type.?) != .optional) {
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
@field(args, tupleFieldName(field_index)) = null;
|
||||
} else {
|
||||
const js_val = info.getArg(@intCast(i), local);
|
||||
@field(args, tupleFieldName(field_index)) = local.jsValueToZig(param.type.?, js_val) catch {
|
||||
return error.InvalidArgument;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ const ModuleEntry = struct {
|
||||
resolver_promise: ?js.Promise.Global = null,
|
||||
};
|
||||
|
||||
fn fromC(c_context: *const v8.Context) *Context {
|
||||
pub fn fromC(c_context: *const v8.Context) *Context {
|
||||
const data = v8.v8__Context__GetEmbedderData(c_context, 1).?;
|
||||
const big_int = js.BigInt{ .handle = @ptrCast(data) };
|
||||
return @ptrFromInt(big_int.getUint64());
|
||||
|
||||
@@ -39,6 +39,14 @@ const JsApis = bridge.JsApis;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const IS_DEBUG = builtin.mode == .Debug;
|
||||
|
||||
fn initClassIds() void {
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
JsApi.Meta.class_id = i;
|
||||
}
|
||||
}
|
||||
|
||||
var class_id_once = std.once(initClassIds);
|
||||
|
||||
// The Env maps to a V8 isolate, which represents a isolated sandbox for
|
||||
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
||||
// and it's where we'll start ExecutionWorlds, which actually execute JavaScript.
|
||||
@@ -73,11 +81,26 @@ global_template: v8.Eternal,
|
||||
// Inspector associated with the Isolate. Exists when CDP is being used.
|
||||
inspector: ?*Inspector,
|
||||
|
||||
// We can store data in a v8::Object's Private data bag. The keys are v8::Private
|
||||
// which an be created once per isolaet.
|
||||
private_symbols: PrivateSymbols,
|
||||
|
||||
pub const InitOpts = struct {
|
||||
with_inspector: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(app: *App, opts: InitOpts) !Env {
|
||||
if (comptime IS_DEBUG) {
|
||||
comptime {
|
||||
// V8 requirement for any data using SetAlignedPointerInInternalField
|
||||
const a = @alignOf(@import("TaggedOpaque.zig"));
|
||||
std.debug.assert(a >= 2 and a % 2 == 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize class IDs once before any V8 work
|
||||
class_id_once.call();
|
||||
|
||||
const allocator = app.allocator;
|
||||
const snapshot = &app.snapshot;
|
||||
|
||||
@@ -114,13 +137,13 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
||||
errdefer allocator.free(templates);
|
||||
|
||||
var global_eternal: v8.Eternal = undefined;
|
||||
var private_symbols: PrivateSymbols = undefined;
|
||||
{
|
||||
var temp_scope: js.HandleScope = undefined;
|
||||
temp_scope.init(isolate);
|
||||
defer temp_scope.deinit();
|
||||
|
||||
inline for (JsApis, 0..) |JsApi, i| {
|
||||
JsApi.Meta.class_id = i;
|
||||
inline for (JsApis, 0..) |_, i| {
|
||||
const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate_handle, snapshot.data_start + i);
|
||||
const function_handle: *const v8.FunctionTemplate = @ptrCast(data);
|
||||
// Make function template eternal
|
||||
@@ -153,6 +176,8 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
||||
.flags = v8.kOnlyInterceptStrings | v8.kNonMasking,
|
||||
});
|
||||
v8.v8__Eternal__New(isolate_handle, @ptrCast(global_template_local), &global_eternal);
|
||||
|
||||
private_symbols = PrivateSymbols.init(isolate_handle);
|
||||
}
|
||||
|
||||
var inspector: ?*js.Inspector = null;
|
||||
@@ -169,8 +194,9 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
||||
.templates = templates,
|
||||
.isolate_params = params,
|
||||
.inspector = inspector,
|
||||
.eternal_function_templates = eternal_function_templates,
|
||||
.global_template = global_eternal,
|
||||
.private_symbols = private_symbols,
|
||||
.eternal_function_templates = eternal_function_templates,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -191,6 +217,7 @@ pub fn deinit(self: *Env) void {
|
||||
|
||||
allocator.free(self.templates);
|
||||
allocator.free(self.eternal_function_templates);
|
||||
self.private_symbols.deinit();
|
||||
|
||||
self.isolate.exit();
|
||||
self.isolate.deinit();
|
||||
@@ -209,14 +236,31 @@ pub fn createContext(self: *Env, page: *Page, enter: bool) !*Context {
|
||||
|
||||
// Get the global template that was created once per isolate
|
||||
const global_template: *const v8.ObjectTemplate = @ptrCast(@alignCast(v8.v8__Eternal__Get(&self.global_template, isolate.handle).?));
|
||||
v8.v8__ObjectTemplate__SetInternalFieldCount(global_template, comptime Snapshot.countInternalFields(Window.JsApi));
|
||||
const v8_context = v8.v8__Context__New(isolate.handle, global_template, null).?;
|
||||
|
||||
// Create the v8::Context and wrap it in a v8::Global
|
||||
var context_global: v8.Global = undefined;
|
||||
v8.v8__Global__New(isolate.handle, v8_context, &context_global);
|
||||
|
||||
// our window wrapped in a v8::Global
|
||||
// get the global object for the context, this maps to our Window
|
||||
const global_obj = v8.v8__Context__Global(v8_context).?;
|
||||
{
|
||||
// Store our TAO inside the internal field of the global object. This
|
||||
// maps the v8::Object -> Zig instance. Almost all objects have this, and
|
||||
// it gets setup automatically as objects are created, but the Window
|
||||
// object already exists in v8 (it's the global) so we manually create
|
||||
// the mapping here.
|
||||
const tao = try context_arena.create(@import("TaggedOpaque.zig"));
|
||||
tao.* = .{
|
||||
.value = @ptrCast(page.window),
|
||||
.prototype_chain = (&Window.JsApi.Meta.prototype_chain).ptr,
|
||||
.prototype_len = @intCast(Window.JsApi.Meta.prototype_chain.len),
|
||||
.subtype = .node, // this probably isn't right, but it's what we've been doing all along
|
||||
};
|
||||
v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
|
||||
}
|
||||
// our window wrapped in a v8::Global
|
||||
var global_global: v8.Global = undefined;
|
||||
v8.v8__Global__New(isolate.handle, global_obj, &global_global);
|
||||
|
||||
@@ -405,3 +449,19 @@ fn oomCallback(c_location: [*c]const u8, details: ?*const v8.OOMDetails) callcon
|
||||
log.fatal(.app, "V8 OOM", .{ .location = location, .detail = detail });
|
||||
@import("../../crash_handler.zig").crash("V8 OOM", .{ .location = location, .detail = detail }, @returnAddress());
|
||||
}
|
||||
|
||||
const PrivateSymbols = struct {
|
||||
const Private = @import("Private.zig");
|
||||
|
||||
child_nodes: Private,
|
||||
|
||||
fn init(isolate: *v8.Isolate) PrivateSymbols {
|
||||
return .{
|
||||
.child_nodes = Private.init(isolate, "child_nodes"),
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *PrivateSymbols) void {
|
||||
self.child_nodes.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,7 +28,11 @@ handle: v8.HandleScope,
|
||||
// value, as v8 will then have taken the address of the function-scopped (and no
|
||||
// longer valid) local.
|
||||
pub fn init(self: *HandleScope, isolate: js.Isolate) void {
|
||||
v8.v8__HandleScope__CONSTRUCT(&self.handle, isolate.handle);
|
||||
self.initWithIsolateHandle(isolate.handle);
|
||||
}
|
||||
|
||||
pub fn initWithIsolateHandle(self: *HandleScope, isolate: *v8.Isolate) void {
|
||||
v8.v8__HandleScope__CONSTRUCT(&self.handle, isolate);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *HandleScope) void {
|
||||
|
||||
@@ -364,9 +364,8 @@ pub fn getTaggedOpaque(value: *const v8.Value) ?*TaggedOpaque {
|
||||
return null;
|
||||
}
|
||||
|
||||
const external_value = v8.v8__Object__GetInternalField(value, 0).?;
|
||||
const external_data = v8.v8__External__Value(external_value).?;
|
||||
return @ptrCast(@alignCast(external_data));
|
||||
const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(value, 0).?;
|
||||
return @ptrCast(@alignCast(tao_ptr));
|
||||
}
|
||||
|
||||
fn cZigStringToString(s: v8.CZigString) ?[]const u8 {
|
||||
|
||||
@@ -75,6 +75,12 @@ pub fn newArray(self: *const Local, len: u32) js.Array {
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a new typed array. Memory is owned by JS context.
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays
|
||||
pub fn createTypedArray(self: *const Local, comptime array_type: js.ArrayType, size: usize) js.ArrayBufferRef(array_type) {
|
||||
return .init(self, size);
|
||||
}
|
||||
|
||||
pub fn runMicrotasks(self: *const Local) void {
|
||||
self.isolate.performMicrotasksCheckpoint();
|
||||
}
|
||||
@@ -181,11 +187,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
|
||||
.subtype = if (@hasDecl(JsApi.Meta, "subtype")) JsApi.Meta.subype else .node,
|
||||
};
|
||||
|
||||
// Skip setting internal field for the global object (Window)
|
||||
// Window accessors get the instance from context.page.window instead
|
||||
// if (resolved.class_id != @import("../webapi/Window.zig").JsApi.Meta.class_id) {
|
||||
v8.v8__Object__SetInternalField(js_obj.handle, 0, isolate.createExternal(tao));
|
||||
// }
|
||||
v8.v8__Object__SetAlignedPointerInInternalField(js_obj.handle, 0, tao);
|
||||
} else {
|
||||
// If the struct is empty, we don't need to do all
|
||||
// the TOA stuff and setting the internal data.
|
||||
@@ -310,6 +312,15 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
|
||||
js.Value => return value,
|
||||
js.Exception => return .{ .local = self, .handle = isolate.throwException(value.handle) },
|
||||
|
||||
js.ArrayBufferRef(.int8).Global, js.ArrayBufferRef(.uint8).Global,
|
||||
js.ArrayBufferRef(.uint8_clamped).Global, js.ArrayBufferRef(.int16).Global,
|
||||
js.ArrayBufferRef(.uint16).Global, js.ArrayBufferRef(.int32).Global,
|
||||
js.ArrayBufferRef(.uint32).Global, js.ArrayBufferRef(.float16).Global,
|
||||
js.ArrayBufferRef(.float32).Global, js.ArrayBufferRef(.float64).Global,
|
||||
=> {
|
||||
return .{ .local = self, .handle = value.local(self).handle };
|
||||
},
|
||||
|
||||
inline
|
||||
js.Function,
|
||||
js.Object,
|
||||
@@ -1164,6 +1175,9 @@ fn _debugValue(self: *const Local, js_val: js.Value, seen: *std.AutoHashMapUnman
|
||||
|
||||
if (js_val.isSymbol()) {
|
||||
const symbol_handle = v8.v8__Symbol__Description(@ptrCast(js_val.handle), self.isolate.handle).?;
|
||||
if (v8.v8__Value__IsUndefined(symbol_handle)) {
|
||||
return writer.writeAll("undefined (symbol)");
|
||||
}
|
||||
return writer.print("{f} (symbol)", .{js.String{ .local = self, .handle = @ptrCast(symbol_handle) }});
|
||||
}
|
||||
const js_val_str = try js_val.toStringSlice();
|
||||
|
||||
42
src/browser/js/Private.zig
Normal file
42
src/browser/js/Private.zig
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const Private = @This();
|
||||
|
||||
// Unlike most types, we always store the Private as a Global. It makes more
|
||||
// sense for this type given how it's used.
|
||||
handle: v8.Global,
|
||||
|
||||
pub fn init(isolate: *v8.Isolate, name: []const u8) Private {
|
||||
const v8_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
const private_handle = v8.v8__Private__New(isolate, v8_name);
|
||||
|
||||
var global: v8.Global = undefined;
|
||||
v8.v8__Global__New(isolate, private_handle, &global);
|
||||
|
||||
return .{
|
||||
.handle = global,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Private) void {
|
||||
v8.v8__Global__Reset(&self.handle);
|
||||
}
|
||||
@@ -202,12 +202,16 @@ pub fn create() !Snapshot {
|
||||
const name = JsApi.Meta.name;
|
||||
const illegal_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
var maybe_result2: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__Set(global_obj, context, illegal_class_name, func, &maybe_result2);
|
||||
v8.v8__Object__DefineOwnProperty(global_obj, context, illegal_class_name, func, 0, &maybe_result2);
|
||||
} else {
|
||||
const name = JsApi.Meta.name;
|
||||
const v8_class_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||
var maybe_result: v8.MaybeBool = undefined;
|
||||
v8.v8__Object__Set(global_obj, context, v8_class_name, func, &maybe_result);
|
||||
var properties: v8.PropertyAttribute = v8.None;
|
||||
if (@hasDecl(JsApi.Meta, "enumerable") and JsApi.Meta.enumerable == false) {
|
||||
properties |= v8.DontEnum;
|
||||
}
|
||||
v8.v8__Object__DefineOwnProperty(global_obj, context, v8_class_name, func, properties, &maybe_result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,9 +433,12 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionT
|
||||
};
|
||||
|
||||
const template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?);
|
||||
if (!@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
||||
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, 1);
|
||||
{
|
||||
const internal_field_count = comptime countInternalFields(JsApi);
|
||||
if (internal_field_count > 0) {
|
||||
const instance_template = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||
v8.v8__ObjectTemplate__SetInternalFieldCount(instance_template, internal_field_count);
|
||||
}
|
||||
}
|
||||
const name_str = if (@hasDecl(JsApi.Meta, "name")) JsApi.Meta.name else @typeName(JsApi);
|
||||
const class_name = v8.v8__String__NewFromUtf8(isolate, name_str.ptr, v8.kNormal, @intCast(name_str.len));
|
||||
@@ -439,6 +446,44 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionT
|
||||
return template;
|
||||
}
|
||||
|
||||
pub fn countInternalFields(comptime JsApi: type) u8 {
|
||||
var last_used_id = 0;
|
||||
var cache_count: u8 = 0;
|
||||
|
||||
inline for (@typeInfo(JsApi).@"struct".decls) |d| {
|
||||
const name: [:0]const u8 = d.name;
|
||||
const value = @field(JsApi, name);
|
||||
const definition = @TypeOf(value);
|
||||
|
||||
switch (definition) {
|
||||
inline bridge.Accessor, bridge.Function => {
|
||||
const cache = value.cache orelse continue;
|
||||
if (cache != .internal) {
|
||||
continue;
|
||||
}
|
||||
// We assert that they are declared in-order. This isn't necessary
|
||||
// but I don't want to do anything fancy to look for gaps or
|
||||
// duplicates.
|
||||
const internal_id = cache.internal;
|
||||
if (internal_id != last_used_id + 1) {
|
||||
@compileError(@typeName(JsApi) ++ "." ++ name ++ " has a non-monotonic cache index");
|
||||
}
|
||||
last_used_id = internal_id;
|
||||
cache_count += 1; // this is just last_used, but it's more explicit this way
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
if (@hasDecl(JsApi.Meta, "empty_with_no_proto")) {
|
||||
return cache_count;
|
||||
}
|
||||
|
||||
// we need cache_count internal fields, + 1 for the TAO pointer (the v8 -> Zig)
|
||||
// mapping) itself.
|
||||
return cache_count + 1;
|
||||
}
|
||||
|
||||
// Attaches JsApi members to the prototype template (normal case)
|
||||
fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void {
|
||||
const target = v8.v8__FunctionTemplate__PrototypeTemplate(template);
|
||||
|
||||
@@ -95,33 +95,6 @@ pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R {
|
||||
}
|
||||
|
||||
const internal_field_count = v8.v8__Object__InternalFieldCount(js_obj_handle);
|
||||
// Special case for Window: the global object doesn't have internal fields
|
||||
// Window instance is stored in context.page.window instead
|
||||
if (internal_field_count == 0) {
|
||||
// Normally, this would be an error. All JsObject that map to a Zig type
|
||||
// are either `empty_with_no_proto` (handled above) or have an
|
||||
// interalFieldCount. The only exception to that is the Window...
|
||||
const isolate = v8.v8__Object__GetIsolate(js_obj_handle).?;
|
||||
const context = js.Context.fromIsolate(.{ .handle = isolate });
|
||||
|
||||
const Window = @import("../webapi/Window.zig");
|
||||
if (T == Window) {
|
||||
return context.page.window;
|
||||
}
|
||||
|
||||
// ... Or the window's prototype.
|
||||
// We could make this all comptime-fancy, but it's easier to hard-code
|
||||
// the EventTarget
|
||||
|
||||
const EventTarget = @import("../webapi/EventTarget.zig");
|
||||
if (T == EventTarget) {
|
||||
return context.page.window._proto;
|
||||
}
|
||||
|
||||
// Type not found in Window's prototype chain
|
||||
return error.InvalidArgument;
|
||||
}
|
||||
|
||||
// if it isn't an empty struct, then the v8.Object should have an
|
||||
// InternalFieldCount > 0, since our toa pointer should be embedded
|
||||
// at index 0 of the internal field count.
|
||||
@@ -133,8 +106,8 @@ pub fn fromJS(comptime R: type, js_obj_handle: *const v8.Object) !R {
|
||||
@compileError("unknown Zig type: " ++ @typeName(R));
|
||||
}
|
||||
|
||||
const internal_field_handle = v8.v8__Object__GetInternalField(js_obj_handle, 0).?;
|
||||
const tao: *TaggedOpaque = @ptrCast(@alignCast(v8.v8__External__Value(internal_field_handle)));
|
||||
const tao_ptr = v8.v8__Object__GetAlignedPointerFromInternalField(js_obj_handle, 0).?;
|
||||
const tao: *TaggedOpaque = @ptrCast(@alignCast(tao_ptr));
|
||||
const expected_type_index = bridge.JsApiLookup.getId(JsApi);
|
||||
|
||||
const prototype_chain = tao.prototype_chain[0..tao.prototype_len];
|
||||
|
||||
@@ -38,11 +38,11 @@ pub fn Builder(comptime T: type) type {
|
||||
return Constructor.init(T, func, opts);
|
||||
}
|
||||
|
||||
pub fn accessor(comptime getter: anytype, comptime setter: anytype, comptime opts: Accessor.Opts) Accessor {
|
||||
pub fn accessor(comptime getter: anytype, comptime setter: anytype, comptime opts: Caller.Function.Opts) Accessor {
|
||||
return Accessor.init(T, getter, setter, opts);
|
||||
}
|
||||
|
||||
pub fn function(comptime func: anytype, comptime opts: Function.Opts) Function {
|
||||
pub fn function(comptime func: anytype, comptime opts: Caller.Function.Opts) Function {
|
||||
return Function.init(T, func, opts);
|
||||
}
|
||||
|
||||
@@ -160,39 +160,17 @@ pub const Constructor = struct {
|
||||
pub const Function = struct {
|
||||
static: bool,
|
||||
arity: usize,
|
||||
cache: ?Caller.Function.Opts.Caching = null,
|
||||
func: *const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void,
|
||||
|
||||
const Opts = struct {
|
||||
static: bool = false,
|
||||
dom_exception: bool = false,
|
||||
as_typed_array: bool = false,
|
||||
null_as_undefined: bool = false,
|
||||
};
|
||||
|
||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Function {
|
||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Caller.Function.Opts) Function {
|
||||
return .{
|
||||
.cache = opts.cache,
|
||||
.static = opts.static,
|
||||
.arity = getArity(@TypeOf(func)),
|
||||
.func = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
defer caller.deinit();
|
||||
|
||||
if (comptime opts.static) {
|
||||
caller.function(T, func, handle.?, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
} else {
|
||||
caller.method(T, func, handle.?, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
}
|
||||
Caller.Function.call(T, handle.?, func, opts);
|
||||
}
|
||||
}.wrap,
|
||||
};
|
||||
@@ -217,42 +195,20 @@ pub const Function = struct {
|
||||
|
||||
pub const Accessor = struct {
|
||||
static: bool = false,
|
||||
cache: ?Caller.Function.Opts.Caching = null,
|
||||
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
||||
setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
||||
|
||||
const Opts = struct {
|
||||
static: bool = false,
|
||||
as_typed_array: bool = false,
|
||||
null_as_undefined: bool = false,
|
||||
dom_exception: bool = false,
|
||||
};
|
||||
|
||||
fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Opts) Accessor {
|
||||
fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Caller.Function.Opts) Accessor {
|
||||
var accessor = Accessor{
|
||||
.cache = opts.cache,
|
||||
.static = opts.static,
|
||||
};
|
||||
|
||||
if (@typeInfo(@TypeOf(getter)) != .null) {
|
||||
accessor.getter = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
defer caller.deinit();
|
||||
|
||||
if (comptime opts.static) {
|
||||
caller.function(T, getter, handle.?, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
} else {
|
||||
caller.method(T, getter, handle.?, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
}
|
||||
Caller.Function.call(T, handle.?, getter, opts);
|
||||
}
|
||||
}.wrap;
|
||||
}
|
||||
@@ -260,16 +216,7 @@ pub const Accessor = struct {
|
||||
if (@typeInfo(@TypeOf(setter)) != .null) {
|
||||
accessor.setter = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
defer caller.deinit();
|
||||
|
||||
caller.method(T, setter, handle.?, .{
|
||||
.dom_exception = opts.dom_exception,
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
Caller.Function.call(T, handle.?, setter, opts);
|
||||
}
|
||||
}.wrap;
|
||||
}
|
||||
@@ -390,11 +337,9 @@ pub const Iterator = struct {
|
||||
.async = opts.async,
|
||||
.func = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
defer caller.deinit();
|
||||
caller.method(T, struct_or_func, handle.?, .{});
|
||||
return Caller.Function.call(T, handle.?, struct_or_func, .{
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
}
|
||||
}.wrap,
|
||||
};
|
||||
@@ -411,12 +356,7 @@ pub const Callable = struct {
|
||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {
|
||||
return .{ .func = struct {
|
||||
fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void {
|
||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?;
|
||||
var caller: Caller = undefined;
|
||||
caller.init(v8_isolate);
|
||||
defer caller.deinit();
|
||||
|
||||
caller.method(T, func, handle.?, .{
|
||||
Caller.Function.call(T, handle.?, func, .{
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
}
|
||||
@@ -811,6 +751,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/element/html/DataList.zig"),
|
||||
@import("../webapi/element/html/Dialog.zig"),
|
||||
@import("../webapi/element/html/Directory.zig"),
|
||||
@import("../webapi/element/html/DList.zig"),
|
||||
@import("../webapi/element/html/Div.zig"),
|
||||
@import("../webapi/element/html/Embed.zig"),
|
||||
@import("../webapi/element/html/FieldSet.zig"),
|
||||
@@ -880,6 +821,9 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/event/MouseEvent.zig"),
|
||||
@import("../webapi/event/PointerEvent.zig"),
|
||||
@import("../webapi/event/KeyboardEvent.zig"),
|
||||
@import("../webapi/event/FocusEvent.zig"),
|
||||
@import("../webapi/event/WheelEvent.zig"),
|
||||
@import("../webapi/event/TextEvent.zig"),
|
||||
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
||||
@import("../webapi/MessageChannel.zig"),
|
||||
@import("../webapi/MessagePort.zig"),
|
||||
@@ -917,11 +861,11 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/VisualViewport.zig"),
|
||||
@import("../webapi/PerformanceObserver.zig"),
|
||||
@import("../webapi/navigation/Navigation.zig"),
|
||||
@import("../webapi/navigation/NavigationEventTarget.zig"),
|
||||
@import("../webapi/navigation/NavigationHistoryEntry.zig"),
|
||||
@import("../webapi/navigation/NavigationActivation.zig"),
|
||||
@import("../webapi/canvas/CanvasRenderingContext2D.zig"),
|
||||
@import("../webapi/canvas/WebGLRenderingContext.zig"),
|
||||
@import("../webapi/SubtleCrypto.zig"),
|
||||
@import("../webapi/Selection.zig"),
|
||||
@import("../webapi/ImageData.zig"),
|
||||
});
|
||||
|
||||
@@ -77,6 +77,97 @@ pub const ArrayBuffer = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const ArrayType = enum(u8) {
|
||||
int8,
|
||||
uint8,
|
||||
uint8_clamped,
|
||||
int16,
|
||||
uint16,
|
||||
int32,
|
||||
uint32,
|
||||
float16,
|
||||
float32,
|
||||
float64,
|
||||
};
|
||||
|
||||
pub fn ArrayBufferRef(comptime kind: ArrayType) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
const BackingInt = switch (kind) {
|
||||
.int8 => i8,
|
||||
.uint8, .uint8_clamped => u8,
|
||||
.int16 => i16,
|
||||
.uint16 => u16,
|
||||
.int32 => i32,
|
||||
.uint32 => u32,
|
||||
.float16 => f16,
|
||||
.float32 => f32,
|
||||
.float64 => f64,
|
||||
};
|
||||
|
||||
local: *const Local,
|
||||
handle: *const v8.Value,
|
||||
|
||||
/// Persisted typed array.
|
||||
pub const Global = struct {
|
||||
handle: v8.Global,
|
||||
|
||||
pub fn deinit(self: *Global) void {
|
||||
v8.v8__Global__Reset(&self.handle);
|
||||
}
|
||||
|
||||
pub fn local(self: *const Global, l: *const Local) Self {
|
||||
return .{ .local = l, .handle = v8.v8__Global__Get(&self.handle, l.isolate.handle).? };
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(local: *const Local, size: usize) Self {
|
||||
const ctx = local.ctx;
|
||||
const isolate = ctx.isolate;
|
||||
const bits = switch (@typeInfo(BackingInt)) {
|
||||
.int => |n| n.bits,
|
||||
.float => |f| f.bits,
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
var array_buffer: *const v8.ArrayBuffer = undefined;
|
||||
if (size == 0) {
|
||||
array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;
|
||||
} else {
|
||||
const buffer_len = size * bits / 8;
|
||||
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, buffer_len).?;
|
||||
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
|
||||
array_buffer = v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?;
|
||||
}
|
||||
|
||||
const handle: *const v8.Value = switch (comptime kind) {
|
||||
.int8 => @ptrCast(v8.v8__Int8Array__New(array_buffer, 0, size).?),
|
||||
.uint8 => @ptrCast(v8.v8__Uint8Array__New(array_buffer, 0, size).?),
|
||||
.uint8_clamped => @ptrCast(v8.v8__Uint8ClampedArray__New(array_buffer, 0, size).?),
|
||||
.int16 => @ptrCast(v8.v8__Int16Array__New(array_buffer, 0, size).?),
|
||||
.uint16 => @ptrCast(v8.v8__Uint16Array__New(array_buffer, 0, size).?),
|
||||
.int32 => @ptrCast(v8.v8__Int32Array__New(array_buffer, 0, size).?),
|
||||
.uint32 => @ptrCast(v8.v8__Uint32Array__New(array_buffer, 0, size).?),
|
||||
.float16 => @ptrCast(v8.v8__Float16Array__New(array_buffer, 0, size).?),
|
||||
.float32 => @ptrCast(v8.v8__Float32Array__New(array_buffer, 0, size).?),
|
||||
.float64 => @ptrCast(v8.v8__Float64Array__New(array_buffer, 0, size).?),
|
||||
};
|
||||
|
||||
return .{ .local = local, .handle = handle };
|
||||
}
|
||||
|
||||
pub fn persist(self: *const Self) !Global {
|
||||
var ctx = self.local.ctx;
|
||||
var global: v8.Global = undefined;
|
||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||
try ctx.global_values.append(ctx.arena, global);
|
||||
|
||||
return .{ .handle = global };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const Exception = struct {
|
||||
local: *const Local,
|
||||
handle: *const v8.Value,
|
||||
@@ -134,8 +225,10 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
|
||||
const values = value.values;
|
||||
const len = values.len;
|
||||
const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, len);
|
||||
const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
|
||||
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
|
||||
if (len > 0) {
|
||||
const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
|
||||
@memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
|
||||
}
|
||||
const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
|
||||
return @ptrCast(v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?);
|
||||
},
|
||||
|
||||
506
src/browser/markdown.zig
Normal file
506
src/browser/markdown.zig
Normal file
@@ -0,0 +1,506 @@
|
||||
// Copyright (C) 2023-2026 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 Page = @import("Page.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
const Node = @import("webapi/Node.zig");
|
||||
|
||||
pub const Opts = struct {
|
||||
// Options for future customization (e.g., dialect)
|
||||
};
|
||||
|
||||
const State = struct {
|
||||
const ListType = enum { ordered, unordered };
|
||||
const ListState = struct {
|
||||
type: ListType,
|
||||
index: usize,
|
||||
};
|
||||
|
||||
list_depth: usize = 0,
|
||||
list_stack: [32]ListState = undefined,
|
||||
in_pre: bool = false,
|
||||
pre_node: ?*Node = null,
|
||||
in_code: bool = false,
|
||||
in_table: bool = false,
|
||||
table_row_index: usize = 0,
|
||||
table_col_count: usize = 0,
|
||||
last_char_was_newline: bool = true,
|
||||
};
|
||||
|
||||
fn isBlock(tag: Element.Tag) bool {
|
||||
return switch (tag) {
|
||||
.p, .div, .section, .article, .header, .footer, .nav, .aside, .h1, .h2, .h3, .h4, .h5, .h6, .ul, .ol, .blockquote, .pre, .table, .hr => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn shouldAddSpacing(tag: Element.Tag) bool {
|
||||
return switch (tag) {
|
||||
.p, .h1, .h2, .h3, .h4, .h5, .h6, .blockquote, .pre, .table => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn ensureNewline(state: *State, writer: *std.Io.Writer) !void {
|
||||
if (!state.last_char_was_newline) {
|
||||
try writer.writeByte('\n');
|
||||
state.last_char_was_newline = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||
_ = opts;
|
||||
var state = State{};
|
||||
try render(node, &state, writer, page);
|
||||
if (!state.last_char_was_newline) {
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
}
|
||||
|
||||
fn render(node: *Node, state: *State, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
||||
switch (node._type) {
|
||||
.document, .document_fragment => {
|
||||
try renderChildren(node, state, writer, page);
|
||||
},
|
||||
.element => |el| {
|
||||
try renderElement(el, state, writer, page);
|
||||
},
|
||||
.cdata => |cd| {
|
||||
if (node.is(Node.CData.Text)) |_| {
|
||||
var text = cd.getData();
|
||||
if (state.in_pre) {
|
||||
if (state.pre_node) |pre| {
|
||||
if (node.parentNode() == pre and node.nextSibling() == null) {
|
||||
text = std.mem.trimRight(u8, text, " \t\r\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
try renderText(text, state, writer);
|
||||
}
|
||||
},
|
||||
else => {}, // Ignore other node types
|
||||
}
|
||||
}
|
||||
|
||||
fn renderChildren(parent: *Node, state: *State, writer: *std.Io.Writer, page: *Page) !void {
|
||||
var it = parent.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
try render(child, state, writer, page);
|
||||
}
|
||||
}
|
||||
|
||||
fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Page) !void {
|
||||
const tag = el.getTag();
|
||||
|
||||
// Skip hidden/metadata elements
|
||||
switch (tag) {
|
||||
.script, .style, .noscript, .template, .head, .meta, .link, .title, .svg => return,
|
||||
else => {},
|
||||
}
|
||||
|
||||
// --- Opening Tag Logic ---
|
||||
|
||||
// Ensure block elements start on a new line (double newline for paragraphs etc)
|
||||
if (isBlock(tag)) {
|
||||
if (!state.in_table) {
|
||||
try ensureNewline(state, writer);
|
||||
if (shouldAddSpacing(tag)) {
|
||||
// Add an extra newline for spacing between blocks
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
}
|
||||
} else if (tag == .li or tag == .tr) {
|
||||
try ensureNewline(state, writer);
|
||||
}
|
||||
|
||||
// Prefixes
|
||||
switch (tag) {
|
||||
.h1 => try writer.writeAll("# "),
|
||||
.h2 => try writer.writeAll("## "),
|
||||
.h3 => try writer.writeAll("### "),
|
||||
.h4 => try writer.writeAll("#### "),
|
||||
.h5 => try writer.writeAll("##### "),
|
||||
.h6 => try writer.writeAll("###### "),
|
||||
.ul => {
|
||||
if (state.list_depth < state.list_stack.len) {
|
||||
state.list_stack[state.list_depth] = .{ .type = .unordered, .index = 0 };
|
||||
state.list_depth += 1;
|
||||
}
|
||||
},
|
||||
.ol => {
|
||||
if (state.list_depth < state.list_stack.len) {
|
||||
state.list_stack[state.list_depth] = .{ .type = .ordered, .index = 1 };
|
||||
state.list_depth += 1;
|
||||
}
|
||||
},
|
||||
.li => {
|
||||
const indent = if (state.list_depth > 0) state.list_depth - 1 else 0;
|
||||
for (0..indent) |_| try writer.writeAll(" ");
|
||||
|
||||
if (state.list_depth > 0) {
|
||||
const current_list = &state.list_stack[state.list_depth - 1];
|
||||
if (current_list.type == .ordered) {
|
||||
try writer.print("{d}. ", .{current_list.index});
|
||||
current_list.index += 1;
|
||||
} else {
|
||||
try writer.writeAll("- ");
|
||||
}
|
||||
} else {
|
||||
try writer.writeAll("- ");
|
||||
}
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.table => {
|
||||
state.in_table = true;
|
||||
state.table_row_index = 0;
|
||||
state.table_col_count = 0;
|
||||
},
|
||||
.tr => {
|
||||
state.table_col_count = 0;
|
||||
try writer.writeByte('|');
|
||||
},
|
||||
.td, .th => {
|
||||
// Note: leading pipe handled by previous cell closing or tr opening
|
||||
state.last_char_was_newline = false;
|
||||
try writer.writeByte(' ');
|
||||
},
|
||||
.blockquote => {
|
||||
try writer.writeAll("> ");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.pre => {
|
||||
try writer.writeAll("```\n");
|
||||
state.in_pre = true;
|
||||
state.pre_node = el.asNode();
|
||||
state.last_char_was_newline = true;
|
||||
},
|
||||
.code => {
|
||||
if (!state.in_pre) {
|
||||
try writer.writeByte('`');
|
||||
state.in_code = true;
|
||||
state.last_char_was_newline = false;
|
||||
}
|
||||
},
|
||||
.b, .strong => {
|
||||
try writer.writeAll("**");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.i, .em => {
|
||||
try writer.writeAll("*");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.s, .del => {
|
||||
try writer.writeAll("~~");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.hr => {
|
||||
try writer.writeAll("---\n");
|
||||
state.last_char_was_newline = true;
|
||||
return; // Void element
|
||||
},
|
||||
.br => {
|
||||
if (state.in_table) {
|
||||
try writer.writeByte(' ');
|
||||
} else {
|
||||
try writer.writeByte('\n');
|
||||
state.last_char_was_newline = true;
|
||||
}
|
||||
return; // Void element
|
||||
},
|
||||
.img => {
|
||||
try writer.writeAll(";
|
||||
if (el.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||
try writer.writeAll(src);
|
||||
}
|
||||
try writer.writeAll(")");
|
||||
state.last_char_was_newline = false;
|
||||
return; // Treat as void
|
||||
},
|
||||
.anchor => {
|
||||
try writer.writeByte('[');
|
||||
try renderChildren(el.asNode(), state, writer, page);
|
||||
try writer.writeAll("](");
|
||||
if (el.getAttributeSafe(comptime .wrap("href"))) |href| {
|
||||
try writer.writeAll(href);
|
||||
}
|
||||
try writer.writeByte(')');
|
||||
state.last_char_was_newline = false;
|
||||
return;
|
||||
},
|
||||
.input => {
|
||||
if (el.getAttributeSafe(comptime .wrap("type"))) |type_attr| {
|
||||
if (std.ascii.eqlIgnoreCase(type_attr, "checkbox")) {
|
||||
if (el.getAttributeSafe(comptime .wrap("checked"))) |_| {
|
||||
try writer.writeAll("[x] ");
|
||||
} else {
|
||||
try writer.writeAll("[ ] ");
|
||||
}
|
||||
state.last_char_was_newline = false;
|
||||
}
|
||||
}
|
||||
return;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
// --- Render Children ---
|
||||
try renderChildren(el.asNode(), state, writer, page);
|
||||
|
||||
// --- Closing Tag Logic ---
|
||||
|
||||
// Suffixes
|
||||
switch (tag) {
|
||||
.pre => {
|
||||
if (!state.last_char_was_newline) {
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
try writer.writeAll("```\n");
|
||||
state.in_pre = false;
|
||||
state.pre_node = null;
|
||||
state.last_char_was_newline = true;
|
||||
},
|
||||
.code => {
|
||||
if (!state.in_pre) {
|
||||
try writer.writeByte('`');
|
||||
state.in_code = false;
|
||||
state.last_char_was_newline = false;
|
||||
}
|
||||
},
|
||||
.b, .strong => {
|
||||
try writer.writeAll("**");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.i, .em => {
|
||||
try writer.writeAll("*");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.s, .del => {
|
||||
try writer.writeAll("~~");
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
.blockquote => {},
|
||||
.ul, .ol => {
|
||||
if (state.list_depth > 0) state.list_depth -= 1;
|
||||
},
|
||||
.table => {
|
||||
state.in_table = false;
|
||||
},
|
||||
.tr => {
|
||||
try writer.writeByte('\n');
|
||||
if (state.table_row_index == 0) {
|
||||
try writer.writeByte('|');
|
||||
var i: usize = 0;
|
||||
while (i < state.table_col_count) : (i += 1) {
|
||||
try writer.writeAll("---|");
|
||||
}
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
state.table_row_index += 1;
|
||||
state.last_char_was_newline = true;
|
||||
},
|
||||
.td, .th => {
|
||||
try writer.writeAll(" |");
|
||||
state.table_col_count += 1;
|
||||
state.last_char_was_newline = false;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
// Post-block newlines
|
||||
if (isBlock(tag)) {
|
||||
if (!state.in_table) {
|
||||
try ensureNewline(state, writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn renderText(text: []const u8, state: *State, writer: *std.Io.Writer) !void {
|
||||
if (text.len == 0) return;
|
||||
|
||||
if (state.in_pre) {
|
||||
try writer.writeAll(text);
|
||||
if (text.len > 0 and text[text.len - 1] == '\n') {
|
||||
state.last_char_was_newline = true;
|
||||
} else {
|
||||
state.last_char_was_newline = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for pure whitespace
|
||||
const is_all_whitespace = for (text) |c| {
|
||||
if (!std.ascii.isWhitespace(c)) break false;
|
||||
} else true;
|
||||
|
||||
if (is_all_whitespace) {
|
||||
if (!state.last_char_was_newline) {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Collapse whitespace
|
||||
var it = std.mem.tokenizeAny(u8, text, " \t\n\r");
|
||||
var first = true;
|
||||
while (it.next()) |word| {
|
||||
if (first) {
|
||||
if (!state.last_char_was_newline) {
|
||||
if (text.len > 0 and std.ascii.isWhitespace(text[0])) {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
|
||||
try escapeMarkdown(writer, word);
|
||||
state.last_char_was_newline = false;
|
||||
first = false;
|
||||
}
|
||||
|
||||
// Handle trailing whitespace from the original text
|
||||
if (!first and !state.last_char_was_newline) {
|
||||
if (text.len > 0 and std.ascii.isWhitespace(text[text.len - 1])) {
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escapeMarkdown(writer: *std.Io.Writer, text: []const u8) !void {
|
||||
for (text) |c| {
|
||||
switch (c) {
|
||||
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '!', '|' => {
|
||||
try writer.writeByte('\\');
|
||||
try writer.writeByte(c);
|
||||
},
|
||||
else => try writer.writeByte(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn testMarkdownHTML(html: []const u8, expected: []const u8) !void {
|
||||
const testing = @import("../testing.zig");
|
||||
const page = try testing.test_session.createPage();
|
||||
defer testing.test_session.removePage();
|
||||
const doc = page.window._document;
|
||||
|
||||
const div = try doc.createElement("div", null, page);
|
||||
try page.parseHtmlAsChildren(div.asNode(), html);
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer aw.deinit();
|
||||
try dump(div.asNode(), .{}, &aw.writer, page);
|
||||
|
||||
try testing.expectString(expected, aw.written());
|
||||
}
|
||||
|
||||
test "markdown: basic" {
|
||||
try testMarkdownHTML("Hello world", "Hello world\n");
|
||||
}
|
||||
|
||||
test "markdown: whitespace" {
|
||||
try testMarkdownHTML("<span>A</span> <span>B</span>", "A B\n");
|
||||
}
|
||||
|
||||
test "markdown: escaping" {
|
||||
try testMarkdownHTML("<p># Not a header</p>", "\n\\# Not a header\n");
|
||||
}
|
||||
|
||||
test "markdown: strikethrough" {
|
||||
try testMarkdownHTML("<s>deleted</s>", "~~deleted~~\n");
|
||||
}
|
||||
|
||||
test "markdown: task list" {
|
||||
try testMarkdownHTML(
|
||||
\\<input type="checkbox" checked><input type="checkbox">
|
||||
, "[x] [ ] \n");
|
||||
}
|
||||
|
||||
test "markdown: ordered list" {
|
||||
try testMarkdownHTML(
|
||||
\\<ol><li>First</li><li>Second</li></ol>
|
||||
, "1. First\n2. Second\n");
|
||||
}
|
||||
|
||||
test "markdown: table" {
|
||||
try testMarkdownHTML(
|
||||
\\<table><thead><tr><th>Head 1</th><th>Head 2</th></tr></thead>
|
||||
\\<tbody><tr><td>Cell 1</td><td>Cell 2</td></tr></tbody></table>
|
||||
,
|
||||
\\
|
||||
\\| Head 1 | Head 2 |
|
||||
\\|---|---|
|
||||
\\| Cell 1 | Cell 2 |
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
test "markdown: nested lists" {
|
||||
try testMarkdownHTML(
|
||||
\\<ul><li>Parent<ul><li>Child</li></ul></li></ul>
|
||||
,
|
||||
\\- Parent
|
||||
\\ - Child
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
test "markdown: blockquote" {
|
||||
try testMarkdownHTML("<blockquote>Hello world</blockquote>", "\n> Hello world\n");
|
||||
}
|
||||
|
||||
test "markdown: links" {
|
||||
try testMarkdownHTML("<a href=\"https://lightpanda.io\">Lightpanda</a>", "[Lightpanda](https://lightpanda.io)\n");
|
||||
}
|
||||
|
||||
test "markdown: images" {
|
||||
try testMarkdownHTML("<img src=\"logo.png\" alt=\"Logo\">", "\n");
|
||||
}
|
||||
|
||||
test "markdown: headings" {
|
||||
try testMarkdownHTML("<h1>Title</h1><h2>Subtitle</h2>",
|
||||
\\
|
||||
\\# Title
|
||||
\\
|
||||
\\## Subtitle
|
||||
\\
|
||||
);
|
||||
}
|
||||
|
||||
test "markdown: code" {
|
||||
try testMarkdownHTML(
|
||||
\\<p>Use git push</p>
|
||||
\\<pre><code>line 1
|
||||
\\line 2</code></pre>
|
||||
,
|
||||
\\
|
||||
\\Use git push
|
||||
\\
|
||||
\\```
|
||||
\\line 1
|
||||
\\line 2
|
||||
\\```
|
||||
\\
|
||||
);
|
||||
}
|
||||
@@ -421,7 +421,16 @@ fn appendBeforeSiblingCallback(ctx: *anyopaque, sibling_ref: *anyopaque, node_or
|
||||
fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e.NodeOrText) !void {
|
||||
const parent = sibling.parentNode() orelse return error.NoParent;
|
||||
const node: *Node = switch (node_or_text.toUnion()) {
|
||||
.node => |cpn| getNode(cpn),
|
||||
.node => |cpn| blk: {
|
||||
const child = getNode(cpn);
|
||||
if (child._parent) |previous_parent| {
|
||||
// A custom element constructor may have inserted the node into the
|
||||
// DOM before the parser officially places it (e.g. via foster
|
||||
// parenting). Detach it first so insertNodeRelative's assertion holds.
|
||||
self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() });
|
||||
}
|
||||
break :blk child;
|
||||
},
|
||||
.text => |txt| try self.page.createTextNode(txt),
|
||||
};
|
||||
try self.page.insertNodeRelative(parent, node, .{ .before = sibling }, .{});
|
||||
|
||||
@@ -16,44 +16,44 @@
|
||||
isRandom(ti8a)
|
||||
}
|
||||
|
||||
// {
|
||||
// let tu16a = new Uint16Array(100)
|
||||
// testing.expectEqual(tu16a, crypto.getRandomValues(tu16a))
|
||||
// isRandom(tu16a)
|
||||
{
|
||||
let tu16a = new Uint16Array(100)
|
||||
testing.expectEqual(tu16a, crypto.getRandomValues(tu16a))
|
||||
isRandom(tu16a)
|
||||
|
||||
// let ti16a = new Int16Array(100)
|
||||
// testing.expectEqual(ti16a, crypto.getRandomValues(ti16a))
|
||||
// isRandom(ti16a)
|
||||
// }
|
||||
let ti16a = new Int16Array(100)
|
||||
testing.expectEqual(ti16a, crypto.getRandomValues(ti16a))
|
||||
isRandom(ti16a)
|
||||
}
|
||||
|
||||
// {
|
||||
// let tu32a = new Uint32Array(100)
|
||||
// testing.expectEqual(tu32a, crypto.getRandomValues(tu32a))
|
||||
// isRandom(tu32a)
|
||||
{
|
||||
let tu32a = new Uint32Array(100)
|
||||
testing.expectEqual(tu32a, crypto.getRandomValues(tu32a))
|
||||
isRandom(tu32a)
|
||||
|
||||
// let ti32a = new Int32Array(100)
|
||||
// testing.expectEqual(ti32a, crypto.getRandomValues(ti32a))
|
||||
// isRandom(ti32a)
|
||||
// }
|
||||
let ti32a = new Int32Array(100)
|
||||
testing.expectEqual(ti32a, crypto.getRandomValues(ti32a))
|
||||
isRandom(ti32a)
|
||||
}
|
||||
|
||||
// {
|
||||
// let tu64a = new BigUint64Array(100)
|
||||
// testing.expectEqual(tu64a, crypto.getRandomValues(tu64a))
|
||||
// isRandom(tu64a)
|
||||
{
|
||||
let tu64a = new BigUint64Array(100)
|
||||
testing.expectEqual(tu64a, crypto.getRandomValues(tu64a))
|
||||
isRandom(tu64a)
|
||||
|
||||
// let ti64a = new BigInt64Array(100)
|
||||
// testing.expectEqual(ti64a, crypto.getRandomValues(ti64a))
|
||||
// isRandom(ti64a)
|
||||
// }
|
||||
let ti64a = new BigInt64Array(100)
|
||||
testing.expectEqual(ti64a, crypto.getRandomValues(ti64a))
|
||||
isRandom(ti64a)
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- <script id="randomUUID">
|
||||
<script id="randomUUID">
|
||||
const uuid = crypto.randomUUID();
|
||||
testing.expectEqual('string', typeof uuid);
|
||||
testing.expectEqual(36, uuid.length);
|
||||
const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
testing.expectEqual(true, regex.test(uuid));
|
||||
</script> -->
|
||||
</script>
|
||||
|
||||
<script id=SubtleCrypto>
|
||||
testing.expectEqual(true, crypto.subtle instanceof SubtleCrypto);
|
||||
@@ -119,3 +119,16 @@
|
||||
testing.expectEqual(16, sharedKey.byteLength);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id="digest">
|
||||
testing.async(async () => {
|
||||
async function hash(algo, data) {
|
||||
const buffer = await window.crypto.subtle.digest(algo, new TextEncoder().encode(data));
|
||||
return [...new Uint8Array(buffer)].map(x => x.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
testing.expectEqual("a6a1e3375239f215f09a156df29c17c7d1ac6722", await hash('sha-1', 'over 9000'));
|
||||
testing.expectEqual("1bc375bb92459685194dda18a4b835f4e2972ec1bde6d9ab3db53fcc584a6580", await hash('sha-256', 'over 9000'));
|
||||
testing.expectEqual("a4260d64c2eea9fd30c1f895c5e48a26d817e19d3a700b61b3ce665864ff4b8e012bd357d345aa614c5f642dab865ea1", await hash('sha-384', 'over 9000'));
|
||||
testing.expectEqual("6cad17e6f3f76680d6dd18ed043b75b4f6e1aa1d08b917294942e882fb6466c3510948c34af8b903ed0725b582b3b39c0e485ae2c1b7dfdb192ee38b79c782b6", await hash('sha-512', 'over 9000'));
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -119,3 +119,33 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="constructor_self_insert_foster_parent">
|
||||
{
|
||||
// Regression: custom element constructor inserting itself (via appendChild) during
|
||||
// innerHTML parsing. When the element is not valid table content, the HTML5 parser
|
||||
// foster-parents it before the <table> via appendBeforeSiblingCallback. That callback
|
||||
// previously didn't check for an existing _parent before calling insertNodeRelative,
|
||||
// causing the "Page.insertNodeRelative parent" assertion to fire.
|
||||
let constructorCalled = 0;
|
||||
let container;
|
||||
|
||||
class CtorSelfInsert extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
constructorCalled++;
|
||||
// Insert self into container so _parent is set before the parser
|
||||
// officially places this element via appendBeforeSiblingCallback.
|
||||
if (container) container.appendChild(this);
|
||||
}
|
||||
}
|
||||
customElements.define('ctor-self-insert', CtorSelfInsert);
|
||||
|
||||
container = document.createElement('div');
|
||||
// ctor-self-insert is not valid table content; the parser foster-parents it
|
||||
// before the <table>, calling appendBeforeSiblingCallback(sibling=table, node=element).
|
||||
// At that point the element already has _parent=container from the constructor.
|
||||
container.innerHTML = '<table><ctor-self-insert></ctor-self-insert></table>';
|
||||
|
||||
testing.expectEqual(1, constructorCalled);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
<script id=documentElement>
|
||||
testing.expectEqual($('#the_body').parentNode, document.documentElement);
|
||||
testing.expectEqual(document.documentElement, document.scrollingElement);
|
||||
</script>
|
||||
|
||||
<script id=title>
|
||||
|
||||
@@ -81,6 +81,172 @@
|
||||
</script>
|
||||
|
||||
|
||||
<script id="focusin_focusout_events">
|
||||
{
|
||||
const input1 = $('#input1');
|
||||
const input2 = $('#input2');
|
||||
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
let events = [];
|
||||
|
||||
input1.addEventListener('focus', () => events.push('focus1'));
|
||||
input1.addEventListener('focusin', () => events.push('focusin1'));
|
||||
input1.addEventListener('blur', () => events.push('blur1'));
|
||||
input1.addEventListener('focusout', () => events.push('focusout1'));
|
||||
input2.addEventListener('focus', () => events.push('focus2'));
|
||||
input2.addEventListener('focusin', () => events.push('focusin2'));
|
||||
|
||||
// Focus input1 — should fire focus then focusin
|
||||
input1.focus();
|
||||
testing.expectEqual('focus1,focusin1', events.join(','));
|
||||
|
||||
// Focus input2 — should fire blur, focusout on input1, then focus, focusin on input2
|
||||
events = [];
|
||||
input2.focus();
|
||||
testing.expectEqual('blur1,focusout1,focus2,focusin2', events.join(','));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="focusin_bubbles">
|
||||
{
|
||||
const input1 = $('#input1');
|
||||
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
let bodyFocusin = 0;
|
||||
let bodyFocus = 0;
|
||||
|
||||
document.body.addEventListener('focusin', () => bodyFocusin++);
|
||||
document.body.addEventListener('focus', () => bodyFocus++);
|
||||
|
||||
input1.focus();
|
||||
|
||||
// focusin should bubble to body, focus should not
|
||||
testing.expectEqual(1, bodyFocusin);
|
||||
testing.expectEqual(0, bodyFocus);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="focusout_bubbles">
|
||||
{
|
||||
const input1 = $('#input1');
|
||||
|
||||
input1.focus();
|
||||
|
||||
let bodyFocusout = 0;
|
||||
let bodyBlur = 0;
|
||||
|
||||
document.body.addEventListener('focusout', () => bodyFocusout++);
|
||||
document.body.addEventListener('blur', () => bodyBlur++);
|
||||
|
||||
input1.blur();
|
||||
|
||||
// focusout should bubble to body, blur should not
|
||||
testing.expectEqual(1, bodyFocusout);
|
||||
testing.expectEqual(0, bodyBlur);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="focus_relatedTarget">
|
||||
{
|
||||
const input1 = $('#input1');
|
||||
const input2 = $('#input2');
|
||||
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
let focusRelated = null;
|
||||
let blurRelated = null;
|
||||
let focusinRelated = null;
|
||||
let focusoutRelated = null;
|
||||
|
||||
input1.addEventListener('blur', (e) => { blurRelated = e.relatedTarget; });
|
||||
input1.addEventListener('focusout', (e) => { focusoutRelated = e.relatedTarget; });
|
||||
input2.addEventListener('focus', (e) => { focusRelated = e.relatedTarget; });
|
||||
input2.addEventListener('focusin', (e) => { focusinRelated = e.relatedTarget; });
|
||||
|
||||
input1.focus();
|
||||
input2.focus();
|
||||
|
||||
// blur/focusout on input1 should have relatedTarget = input2 (gaining focus)
|
||||
testing.expectEqual(input2, blurRelated);
|
||||
testing.expectEqual(input2, focusoutRelated);
|
||||
|
||||
// focus/focusin on input2 should have relatedTarget = input1 (losing focus)
|
||||
testing.expectEqual(input1, focusRelated);
|
||||
testing.expectEqual(input1, focusinRelated);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="blur_relatedTarget_null">
|
||||
{
|
||||
const btn = $('#btn1');
|
||||
|
||||
btn.focus();
|
||||
|
||||
let blurRelated = 'not_set';
|
||||
btn.addEventListener('blur', (e) => { blurRelated = e.relatedTarget; });
|
||||
btn.blur();
|
||||
|
||||
// blur without moving to another element should have relatedTarget = null
|
||||
testing.expectEqual(null, blurRelated);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="focus_event_properties">
|
||||
{
|
||||
const input1 = $('#input1');
|
||||
const input2 = $('#input2');
|
||||
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
let focusEvent = null;
|
||||
let focusinEvent = null;
|
||||
let blurEvent = null;
|
||||
let focusoutEvent = null;
|
||||
|
||||
input1.addEventListener('blur', (e) => { blurEvent = e; });
|
||||
input1.addEventListener('focusout', (e) => { focusoutEvent = e; });
|
||||
input2.addEventListener('focus', (e) => { focusEvent = e; });
|
||||
input2.addEventListener('focusin', (e) => { focusinEvent = e; });
|
||||
|
||||
input1.focus();
|
||||
input2.focus();
|
||||
|
||||
// All four should be FocusEvent instances
|
||||
testing.expectEqual(true, blurEvent instanceof FocusEvent);
|
||||
testing.expectEqual(true, focusoutEvent instanceof FocusEvent);
|
||||
testing.expectEqual(true, focusEvent instanceof FocusEvent);
|
||||
testing.expectEqual(true, focusinEvent instanceof FocusEvent);
|
||||
|
||||
// All four should be composed per spec
|
||||
testing.expectEqual(true, blurEvent.composed);
|
||||
testing.expectEqual(true, focusoutEvent.composed);
|
||||
testing.expectEqual(true, focusEvent.composed);
|
||||
testing.expectEqual(true, focusinEvent.composed);
|
||||
|
||||
// None should be cancelable
|
||||
testing.expectEqual(false, blurEvent.cancelable);
|
||||
testing.expectEqual(false, focusoutEvent.cancelable);
|
||||
testing.expectEqual(false, focusEvent.cancelable);
|
||||
testing.expectEqual(false, focusinEvent.cancelable);
|
||||
|
||||
// blur/focus don't bubble, focusin/focusout do
|
||||
testing.expectEqual(false, blurEvent.bubbles);
|
||||
testing.expectEqual(true, focusoutEvent.bubbles);
|
||||
testing.expectEqual(false, focusEvent.bubbles);
|
||||
testing.expectEqual(true, focusinEvent.bubbles);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="focus_disconnected">
|
||||
{
|
||||
const focused = document.activeElement;
|
||||
@@ -88,3 +254,46 @@
|
||||
testing.expectEqual(focused, document.activeElement);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="click_focuses_element">
|
||||
{
|
||||
const input1 = $('#input1');
|
||||
const input2 = $('#input2');
|
||||
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
let focusCount = 0;
|
||||
let blurCount = 0;
|
||||
|
||||
input1.addEventListener('focus', () => focusCount++);
|
||||
input1.addEventListener('blur', () => blurCount++);
|
||||
input2.addEventListener('focus', () => focusCount++);
|
||||
|
||||
// Click input1 — should focus it and fire focus event
|
||||
input1.click();
|
||||
testing.expectEqual(input1, document.activeElement);
|
||||
testing.expectEqual(1, focusCount);
|
||||
testing.expectEqual(0, blurCount);
|
||||
|
||||
// Click input2 — should move focus, fire blur on input1 and focus on input2
|
||||
input2.click();
|
||||
testing.expectEqual(input2, document.activeElement);
|
||||
testing.expectEqual(2, focusCount);
|
||||
testing.expectEqual(1, blurCount);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="click_focuses_button">
|
||||
{
|
||||
const btn = $('#btn1');
|
||||
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
btn.click();
|
||||
testing.expectEqual(btn, document.activeElement);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -270,3 +270,36 @@
|
||||
testing.expectEqual('rect', document.querySelector('svg g rect').tagName);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=special>
|
||||
testing.expectEqual(null, document.querySelector('\\'));
|
||||
|
||||
testing.expectEqual(null, document.querySelector('div\\'));
|
||||
testing.expectEqual(null, document.querySelector('.test-class\\'));
|
||||
testing.expectEqual(null, document.querySelector('#byId\\'));
|
||||
</script>
|
||||
|
||||
<div class="café">Non-ASCII class 1</div>
|
||||
<div class="日本語">Non-ASCII class 2</div>
|
||||
<span id="niño">Non-ASCII ID 1</span>
|
||||
<p id="🎨">Non-ASCII ID 2</p>
|
||||
|
||||
<script id=nonAsciiSelectors>
|
||||
testing.expectEqual('Non-ASCII class 1', document.querySelector('.café').textContent);
|
||||
testing.expectEqual('Non-ASCII class 2', document.querySelector('.日本語').textContent);
|
||||
|
||||
testing.expectEqual('Non-ASCII ID 1', document.querySelector('#niño').textContent);
|
||||
testing.expectEqual('Non-ASCII ID 2', document.querySelector('#🎨').textContent);
|
||||
|
||||
testing.expectEqual('Non-ASCII class 1', document.querySelector('div.café').textContent);
|
||||
testing.expectEqual('Non-ASCII ID 1', document.querySelector('span#niño').textContent);
|
||||
</script>
|
||||
|
||||
<span id=".,:!">Punctuation test</span>
|
||||
|
||||
<script id=escapedPunctuation>
|
||||
{
|
||||
// Test escaped punctuation in ID selectors
|
||||
testing.expectEqual('Punctuation test', document.querySelector('#\\.\\,\\:\\!').textContent);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -93,6 +93,29 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=replace_errors>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.className = 'foo bar';
|
||||
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual('SyntaxError', err.name);
|
||||
}, () => div.classList.replace('', 'baz'));
|
||||
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual('SyntaxError', err.name);
|
||||
}, () => div.classList.replace('foo', ''));
|
||||
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual('InvalidCharacterError', err.name);
|
||||
}, () => div.classList.replace('foo bar', 'baz'));
|
||||
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual('InvalidCharacterError', err.name);
|
||||
}, () => div.classList.replace('foo', 'bar baz'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=item>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
@@ -166,6 +189,29 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=classList_assignment>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
|
||||
// Direct assignment should work (equivalent to classList.value = ...)
|
||||
div.classList = 'foo bar baz';
|
||||
testing.expectEqual('foo bar baz', div.className);
|
||||
testing.expectEqual(3, div.classList.length);
|
||||
testing.expectEqual(true, div.classList.contains('foo'));
|
||||
|
||||
// Assigning again should replace
|
||||
div.classList = 'qux';
|
||||
testing.expectEqual('qux', div.className);
|
||||
testing.expectEqual(1, div.classList.length);
|
||||
testing.expectEqual(false, div.classList.contains('foo'));
|
||||
|
||||
// Empty assignment
|
||||
div.classList = '';
|
||||
testing.expectEqual('', div.className);
|
||||
testing.expectEqual(0, div.classList.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=errors>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
|
||||
@@ -121,6 +121,29 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="propertyAssignment">
|
||||
{
|
||||
const div = $('#test-div');
|
||||
div.style.cssText = '';
|
||||
|
||||
// camelCase assignment
|
||||
div.style.opacity = '0.5';
|
||||
testing.expectEqual('0.5', div.style.opacity);
|
||||
|
||||
// bracket notation assignment
|
||||
div.style['filter'] = 'blur(5px)';
|
||||
testing.expectEqual('blur(5px)', div.style.filter);
|
||||
|
||||
// numeric value coerced to string
|
||||
div.style.opacity = 1;
|
||||
testing.expectEqual('1', div.style.opacity);
|
||||
|
||||
// assigning method names should be ignored (not intercepted)
|
||||
div.style.setProperty('color', 'blue');
|
||||
testing.expectEqual('blue', div.style.color);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="prototypeChainCheck">
|
||||
{
|
||||
const div = $('#test-div');
|
||||
|
||||
123
src/browser/tests/element/get_elements_by_tag_name_ns.html
Normal file
123
src/browser/tests/element/get_elements_by_tag_name_ns.html
Normal file
@@ -0,0 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<div id="container" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<div id="div1">div1</div>
|
||||
<p id="p1">p1</p>
|
||||
<div id="div2">div2</div>
|
||||
</div>
|
||||
|
||||
<svg id="svgContainer" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
||||
<circle id="circle1" cx="50" cy="50" r="40"/>
|
||||
<rect id="rect1" x="10" y="10" width="30" height="30"/>
|
||||
<circle id="circle2" cx="25" cy="25" r="10"/>
|
||||
</svg>
|
||||
|
||||
<div id="mixed">
|
||||
<div id="htmlDiv" xmlns="http://www.w3.org/1999/xhtml">HTML div</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<circle id="svgCircle" cx="10" cy="10" r="5"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<script id=basic>
|
||||
{
|
||||
const htmlNS = "http://www.w3.org/1999/xhtml";
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
|
||||
// Test HTML namespace
|
||||
const htmlDivs = document.getElementsByTagNameNS(htmlNS, 'div');
|
||||
testing.expectEqual(true, htmlDivs instanceof HTMLCollection);
|
||||
testing.expectEqual(5, htmlDivs.length); // container, div1, div2, mixed, htmlDiv
|
||||
|
||||
const htmlPs = document.getElementsByTagNameNS(htmlNS, 'p');
|
||||
testing.expectEqual(1, htmlPs.length);
|
||||
testing.expectEqual('p1', htmlPs[0].id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=svgNamespace>
|
||||
{
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
|
||||
const circles = document.getElementsByTagNameNS(svgNS, 'circle');
|
||||
testing.expectEqual(3, circles.length); // circle1, circle2, svgCircle
|
||||
testing.expectEqual('circle1', circles[0].id);
|
||||
testing.expectEqual('circle2', circles[1].id);
|
||||
testing.expectEqual('svgCircle', circles[2].id);
|
||||
|
||||
const rects = document.getElementsByTagNameNS(svgNS, 'rect');
|
||||
testing.expectEqual(1, rects.length);
|
||||
testing.expectEqual('rect1', rects[0].id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=nullNamespace>
|
||||
{
|
||||
// Null namespace should match elements with null namespace
|
||||
const nullNsElements = document.getElementsByTagNameNS(null, 'div');
|
||||
testing.expectEqual(0, nullNsElements.length); // Our divs are in HTML namespace
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=wildcardNamespace>
|
||||
{
|
||||
// Wildcard namespace "*" should match all namespaces
|
||||
const allDivs = document.getElementsByTagNameNS('*', 'div');
|
||||
testing.expectEqual(5, allDivs.length); // All divs regardless of namespace
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=wildcardLocalName>
|
||||
{
|
||||
const htmlNS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
// Wildcard local name should match all elements in that namespace
|
||||
const allHtmlElements = document.getElementsByTagNameNS(htmlNS, '*');
|
||||
testing.expectEqual(true, allHtmlElements.length > 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=caseSensitive>
|
||||
{
|
||||
const htmlNS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
// getElementsByTagNameNS is case-sensitive for local names
|
||||
const lowerDivs = document.getElementsByTagNameNS(htmlNS, 'div');
|
||||
const upperDivs = document.getElementsByTagNameNS(htmlNS, 'DIV');
|
||||
|
||||
testing.expectEqual(5, lowerDivs.length);
|
||||
testing.expectEqual(0, upperDivs.length); // Should be 0 because it's case-sensitive
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=unknownNamespace>
|
||||
{
|
||||
// Unknown namespace should still work
|
||||
const unknownNs = document.getElementsByTagNameNS('http://example.com/unknown', 'div');
|
||||
testing.expectEqual(0, unknownNs.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=emptyResult>
|
||||
{
|
||||
const htmlNS = "http://www.w3.org/1999/xhtml";
|
||||
const svgNS = "http://www.w3.org/2000/svg";
|
||||
|
||||
testing.expectEqual(0, document.getElementsByTagNameNS(htmlNS, 'nonexistent').length);
|
||||
testing.expectEqual(0, document.getElementsByTagNameNS(svgNS, 'nonexistent').length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=elementMethod>
|
||||
{
|
||||
const htmlNS = "http://www.w3.org/1999/xhtml";
|
||||
const container = document.getElementById('container');
|
||||
|
||||
// getElementsByTagNameNS on element should only search descendants
|
||||
const divsInContainer = container.getElementsByTagNameNS(htmlNS, 'div');
|
||||
testing.expectEqual(2, divsInContainer.length); // div1, div2 (not container itself)
|
||||
testing.expectEqual('div1', divsInContainer[0].id);
|
||||
testing.expectEqual('div2', divsInContainer[1].id);
|
||||
}
|
||||
</script>
|
||||
35
src/browser/tests/element/html/fieldset.html
Normal file
35
src/browser/tests/element/html/fieldset.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<fieldset id="fs1" disabled name="group1">
|
||||
<input type="text">
|
||||
</fieldset>
|
||||
<fieldset id="fs2">
|
||||
<input type="text">
|
||||
</fieldset>
|
||||
|
||||
<script id="disabled">
|
||||
{
|
||||
const fs1 = document.getElementById('fs1');
|
||||
testing.expectEqual(true, fs1.disabled);
|
||||
|
||||
fs1.disabled = false;
|
||||
testing.expectEqual(false, fs1.disabled);
|
||||
|
||||
const fs2 = document.getElementById('fs2');
|
||||
testing.expectEqual(false, fs2.disabled);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="name">
|
||||
{
|
||||
const fs1 = document.getElementById('fs1');
|
||||
testing.expectEqual('group1', fs1.name);
|
||||
|
||||
fs1.name = 'updated';
|
||||
testing.expectEqual('updated', fs1.name);
|
||||
|
||||
const fs2 = document.getElementById('fs2');
|
||||
testing.expectEqual('', fs2.name);
|
||||
}
|
||||
</script>
|
||||
56
src/browser/tests/element/html/htmlelement-props.html
Normal file
56
src/browser/tests/element/html/htmlelement-props.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<div id="d1" hidden>Hidden div</div>
|
||||
<div id="d2">Visible div</div>
|
||||
<input id="i1" tabindex="5">
|
||||
<div id="d3">No tabindex</div>
|
||||
|
||||
<script id="hidden">
|
||||
{
|
||||
const d1 = document.getElementById('d1');
|
||||
testing.expectEqual(true, d1.hidden);
|
||||
|
||||
d1.hidden = false;
|
||||
testing.expectEqual(false, d1.hidden);
|
||||
|
||||
const d2 = document.getElementById('d2');
|
||||
testing.expectEqual(false, d2.hidden);
|
||||
|
||||
d2.hidden = true;
|
||||
testing.expectEqual(true, d2.hidden);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="tabIndex">
|
||||
{
|
||||
const i1 = document.getElementById('i1');
|
||||
testing.expectEqual(5, i1.tabIndex);
|
||||
|
||||
i1.tabIndex = 10;
|
||||
testing.expectEqual(10, i1.tabIndex);
|
||||
|
||||
// Non-interactive elements default to -1
|
||||
const d3 = document.getElementById('d3');
|
||||
testing.expectEqual(-1, d3.tabIndex);
|
||||
|
||||
d3.tabIndex = 0;
|
||||
testing.expectEqual(0, d3.tabIndex);
|
||||
|
||||
// Interactive elements default to 0 per spec
|
||||
const input = document.createElement('input');
|
||||
testing.expectEqual(0, input.tabIndex);
|
||||
|
||||
const button = document.createElement('button');
|
||||
testing.expectEqual(0, button.tabIndex);
|
||||
|
||||
const a = document.createElement('a');
|
||||
testing.expectEqual(0, a.tabIndex);
|
||||
|
||||
const select = document.createElement('select');
|
||||
testing.expectEqual(0, select.tabIndex);
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
testing.expectEqual(0, textarea.tabIndex);
|
||||
}
|
||||
</script>
|
||||
@@ -98,6 +98,22 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="complete">
|
||||
{
|
||||
// Image with no src is complete per spec
|
||||
const img = document.createElement('img');
|
||||
testing.expectEqual(true, img.complete);
|
||||
|
||||
// Image with src is also complete (headless browser, no actual fetch)
|
||||
img.src = 'test.png';
|
||||
testing.expectEqual(true, img.complete);
|
||||
|
||||
// Image constructor also complete
|
||||
const img2 = new Image();
|
||||
testing.expectEqual(true, img2.complete);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="load-trigger-event">
|
||||
{
|
||||
const img = document.createElement("img");
|
||||
|
||||
87
src/browser/tests/element/html/input-attrs.html
Normal file
87
src/browser/tests/element/html/input-attrs.html
Normal file
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<input id="i1" placeholder="Enter name" min="0" max="100" step="5" autocomplete="email">
|
||||
<input id="i2" type="file" multiple>
|
||||
<input id="i3">
|
||||
|
||||
<script id="placeholder">
|
||||
{
|
||||
const i1 = document.getElementById('i1');
|
||||
testing.expectEqual('Enter name', i1.placeholder);
|
||||
|
||||
i1.placeholder = 'Updated';
|
||||
testing.expectEqual('Updated', i1.placeholder);
|
||||
|
||||
const i3 = document.getElementById('i3');
|
||||
testing.expectEqual('', i3.placeholder);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="min">
|
||||
{
|
||||
const i1 = document.getElementById('i1');
|
||||
testing.expectEqual('0', i1.min);
|
||||
|
||||
i1.min = '10';
|
||||
testing.expectEqual('10', i1.min);
|
||||
|
||||
const i3 = document.getElementById('i3');
|
||||
testing.expectEqual('', i3.min);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="max">
|
||||
{
|
||||
const i1 = document.getElementById('i1');
|
||||
testing.expectEqual('100', i1.max);
|
||||
|
||||
i1.max = '200';
|
||||
testing.expectEqual('200', i1.max);
|
||||
|
||||
const i3 = document.getElementById('i3');
|
||||
testing.expectEqual('', i3.max);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="step">
|
||||
{
|
||||
const i1 = document.getElementById('i1');
|
||||
testing.expectEqual('5', i1.step);
|
||||
|
||||
i1.step = '0.5';
|
||||
testing.expectEqual('0.5', i1.step);
|
||||
|
||||
const i3 = document.getElementById('i3');
|
||||
testing.expectEqual('', i3.step);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="multiple">
|
||||
{
|
||||
const i2 = document.getElementById('i2');
|
||||
testing.expectEqual(true, i2.multiple);
|
||||
|
||||
i2.multiple = false;
|
||||
testing.expectEqual(false, i2.multiple);
|
||||
|
||||
const i3 = document.getElementById('i3');
|
||||
testing.expectEqual(false, i3.multiple);
|
||||
|
||||
i3.multiple = true;
|
||||
testing.expectEqual(true, i3.multiple);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="autocomplete">
|
||||
{
|
||||
const i1 = document.getElementById('i1');
|
||||
testing.expectEqual('email', i1.autocomplete);
|
||||
|
||||
i1.autocomplete = 'off';
|
||||
testing.expectEqual('off', i1.autocomplete);
|
||||
|
||||
const i3 = document.getElementById('i3');
|
||||
testing.expectEqual('', i3.autocomplete);
|
||||
}
|
||||
</script>
|
||||
@@ -221,7 +221,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- <script id="defaultChecked">
|
||||
<script id="defaultChecked">
|
||||
testing.expectEqual(true, $('#check1').defaultChecked)
|
||||
testing.expectEqual(false, $('#check2').defaultChecked)
|
||||
testing.expectEqual(true, $('#radio1').defaultChecked)
|
||||
@@ -493,4 +493,4 @@
|
||||
input_checked.defaultChecked = true;
|
||||
testing.expectEqual(false, input_checked.checked);
|
||||
}
|
||||
</script> -->
|
||||
</script>
|
||||
|
||||
283
src/browser/tests/element/html/input_click.html
Normal file
283
src/browser/tests/element/html/input_click.html
Normal file
@@ -0,0 +1,283 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<!-- Checkbox click tests -->
|
||||
<input id="checkbox1" type="checkbox">
|
||||
<input id="checkbox2" type="checkbox" checked>
|
||||
<input id="checkbox_disabled" type="checkbox" disabled>
|
||||
|
||||
<!-- Radio click tests -->
|
||||
<input id="radio1" type="radio" name="clickgroup" checked>
|
||||
<input id="radio2" type="radio" name="clickgroup">
|
||||
<input id="radio3" type="radio" name="clickgroup">
|
||||
<input id="radio_disabled" type="radio" name="clickgroup" disabled>
|
||||
|
||||
<script id="checkbox_click_toggles">
|
||||
{
|
||||
const cb = $('#checkbox1');
|
||||
testing.expectEqual(false, cb.checked);
|
||||
|
||||
cb.click();
|
||||
testing.expectEqual(true, cb.checked);
|
||||
|
||||
cb.click();
|
||||
testing.expectEqual(false, cb.checked);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="checkbox_click_preventDefault_reverts">
|
||||
{
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
testing.expectEqual(false, cb.checked);
|
||||
|
||||
cb.addEventListener('click', (e) => {
|
||||
testing.expectEqual(true, cb.checked, 'checkbox should be checked during click handler');
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
cb.click();
|
||||
testing.expectEqual(false, cb.checked, 'checkbox should revert after preventDefault');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="checkbox_click_events_order">
|
||||
{
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
document.body.appendChild(cb);
|
||||
|
||||
const events = [];
|
||||
|
||||
cb.addEventListener('click', () => events.push('click'));
|
||||
cb.addEventListener('input', () => events.push('input'));
|
||||
cb.addEventListener('change', () => events.push('change'));
|
||||
|
||||
cb.click();
|
||||
|
||||
testing.expectEqual(3, events.length);
|
||||
testing.expectEqual('click', events[0]);
|
||||
testing.expectEqual('input', events[1]);
|
||||
testing.expectEqual('change', events[2]);
|
||||
|
||||
document.body.removeChild(cb);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="checkbox_click_preventDefault_no_input_change">
|
||||
{
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
document.body.appendChild(cb);
|
||||
|
||||
const events = [];
|
||||
|
||||
cb.addEventListener('click', (e) => {
|
||||
events.push('click');
|
||||
e.preventDefault();
|
||||
});
|
||||
cb.addEventListener('input', () => events.push('input'));
|
||||
cb.addEventListener('change', () => events.push('change'));
|
||||
|
||||
cb.click();
|
||||
|
||||
testing.expectEqual(1, events.length, 'only click event should fire');
|
||||
testing.expectEqual('click', events[0]);
|
||||
|
||||
document.body.removeChild(cb);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="checkbox_click_state_visible_in_handler">
|
||||
{
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.checked = true;
|
||||
|
||||
cb.addEventListener('click', (e) => {
|
||||
testing.expectEqual(false, cb.checked, 'should see toggled state in handler');
|
||||
e.preventDefault();
|
||||
testing.expectEqual(false, cb.checked, 'should still be toggled after preventDefault in handler');
|
||||
});
|
||||
|
||||
cb.click();
|
||||
testing.expectEqual(true, cb.checked, 'should revert to original state after handler completes');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="radio_click_checks_clicked">
|
||||
{
|
||||
const r1 = $('#radio1');
|
||||
const r2 = $('#radio2');
|
||||
|
||||
testing.expectEqual(true, r1.checked);
|
||||
testing.expectEqual(false, r2.checked);
|
||||
|
||||
r2.click();
|
||||
testing.expectEqual(false, r1.checked);
|
||||
testing.expectEqual(true, r2.checked);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="radio_click_preventDefault_reverts">
|
||||
{
|
||||
const r1 = document.createElement('input');
|
||||
r1.type = 'radio';
|
||||
r1.name = 'testgroup';
|
||||
r1.checked = true;
|
||||
|
||||
const r2 = document.createElement('input');
|
||||
r2.type = 'radio';
|
||||
r2.name = 'testgroup';
|
||||
|
||||
document.body.appendChild(r1);
|
||||
document.body.appendChild(r2);
|
||||
|
||||
r2.addEventListener('click', (e) => {
|
||||
testing.expectEqual(false, r1.checked, 'r1 should be unchecked during click handler');
|
||||
testing.expectEqual(true, r2.checked, 'r2 should be checked during click handler');
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
r2.click();
|
||||
|
||||
testing.expectEqual(true, r1.checked, 'r1 should be restored after preventDefault');
|
||||
testing.expectEqual(false, r2.checked, 'r2 should revert after preventDefault');
|
||||
|
||||
document.body.removeChild(r1);
|
||||
document.body.removeChild(r2);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="radio_click_events_order">
|
||||
{
|
||||
const r = document.createElement('input');
|
||||
r.type = 'radio';
|
||||
r.name = 'eventtest';
|
||||
document.body.appendChild(r);
|
||||
|
||||
const events = [];
|
||||
|
||||
r.addEventListener('click', () => events.push('click'));
|
||||
r.addEventListener('input', () => events.push('input'));
|
||||
r.addEventListener('change', () => events.push('change'));
|
||||
|
||||
r.click();
|
||||
|
||||
testing.expectEqual(3, events.length);
|
||||
testing.expectEqual('click', events[0]);
|
||||
testing.expectEqual('input', events[1]);
|
||||
testing.expectEqual('change', events[2]);
|
||||
|
||||
document.body.removeChild(r);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="radio_click_already_checked_no_events">
|
||||
{
|
||||
const r = document.createElement('input');
|
||||
r.type = 'radio';
|
||||
r.name = 'alreadytest';
|
||||
r.checked = true;
|
||||
document.body.appendChild(r);
|
||||
|
||||
const events = [];
|
||||
|
||||
r.addEventListener('click', () => events.push('click'));
|
||||
r.addEventListener('input', () => events.push('input'));
|
||||
r.addEventListener('change', () => events.push('change'));
|
||||
|
||||
r.click();
|
||||
|
||||
testing.expectEqual(1, events.length, 'only click event should fire for already-checked radio');
|
||||
testing.expectEqual('click', events[0]);
|
||||
|
||||
document.body.removeChild(r);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="disabled_checkbox_no_click">
|
||||
{
|
||||
const cb = $('#checkbox_disabled');
|
||||
const events = [];
|
||||
|
||||
cb.addEventListener('click', () => events.push('click'));
|
||||
cb.addEventListener('input', () => events.push('input'));
|
||||
cb.addEventListener('change', () => events.push('change'));
|
||||
|
||||
cb.click();
|
||||
|
||||
testing.expectEqual(0, events.length, 'disabled checkbox should not fire any events');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="disabled_radio_no_click">
|
||||
{
|
||||
const r = $('#radio_disabled');
|
||||
const events = [];
|
||||
|
||||
r.addEventListener('click', () => events.push('click'));
|
||||
r.addEventListener('input', () => events.push('input'));
|
||||
r.addEventListener('change', () => events.push('change'));
|
||||
|
||||
r.click();
|
||||
|
||||
testing.expectEqual(0, events.length, 'disabled radio should not fire any events');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="input_and_change_are_trusted">
|
||||
{
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
document.body.appendChild(cb);
|
||||
|
||||
let inputEvent = null;
|
||||
let changeEvent = null;
|
||||
|
||||
cb.addEventListener('input', (e) => inputEvent = e);
|
||||
cb.addEventListener('change', (e) => changeEvent = e);
|
||||
|
||||
cb.click();
|
||||
|
||||
testing.expectEqual(true, inputEvent.isTrusted, 'input event should be trusted');
|
||||
testing.expectEqual(true, inputEvent.bubbles, 'input event should bubble');
|
||||
testing.expectEqual(false, inputEvent.cancelable, 'input event should not be cancelable');
|
||||
|
||||
testing.expectEqual(true, changeEvent.isTrusted, 'change event should be trusted');
|
||||
testing.expectEqual(true, changeEvent.bubbles, 'change event should bubble');
|
||||
testing.expectEqual(false, changeEvent.cancelable, 'change event should not be cancelable');
|
||||
|
||||
document.body.removeChild(cb);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="multiple_radios_click_sequence">
|
||||
{
|
||||
const r1 = $('#radio1');
|
||||
const r2 = $('#radio2');
|
||||
const r3 = $('#radio3');
|
||||
|
||||
// Reset to known state
|
||||
r1.checked = true;
|
||||
|
||||
testing.expectEqual(true, r1.checked);
|
||||
testing.expectEqual(false, r2.checked);
|
||||
testing.expectEqual(false, r3.checked);
|
||||
|
||||
r2.click();
|
||||
testing.expectEqual(false, r1.checked);
|
||||
testing.expectEqual(true, r2.checked);
|
||||
testing.expectEqual(false, r3.checked);
|
||||
|
||||
r3.click();
|
||||
testing.expectEqual(false, r1.checked);
|
||||
testing.expectEqual(false, r2.checked);
|
||||
testing.expectEqual(true, r3.checked);
|
||||
|
||||
r1.click();
|
||||
testing.expectEqual(true, r1.checked);
|
||||
testing.expectEqual(false, r2.checked);
|
||||
testing.expectEqual(false, r3.checked);
|
||||
}
|
||||
</script>
|
||||
18
src/browser/tests/element/html/label.html
Normal file
18
src/browser/tests/element/html/label.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<label id="l1" for="input1">Name</label>
|
||||
<input id="input1">
|
||||
|
||||
<script id="htmlFor">
|
||||
{
|
||||
const l1 = document.getElementById('l1');
|
||||
testing.expectEqual('input1', l1.htmlFor);
|
||||
|
||||
l1.htmlFor = 'input2';
|
||||
testing.expectEqual('input2', l1.htmlFor);
|
||||
|
||||
const l2 = document.createElement('label');
|
||||
testing.expectEqual('', l2.htmlFor);
|
||||
}
|
||||
</script>
|
||||
23
src/browser/tests/element/html/li.html
Normal file
23
src/browser/tests/element/html/li.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<ol>
|
||||
<li id="li1" value="5">Item</li>
|
||||
<li id="li2">Item</li>
|
||||
</ol>
|
||||
|
||||
<script id="value">
|
||||
{
|
||||
const li1 = document.getElementById('li1');
|
||||
testing.expectEqual(5, li1.value);
|
||||
|
||||
li1.value = 10;
|
||||
testing.expectEqual(10, li1.value);
|
||||
|
||||
const li2 = document.getElementById('li2');
|
||||
testing.expectEqual(0, li2.value);
|
||||
|
||||
li2.value = -3;
|
||||
testing.expectEqual(-3, li2.value);
|
||||
}
|
||||
</script>
|
||||
@@ -9,4 +9,13 @@
|
||||
|
||||
l2.href = '/over/9000';
|
||||
testing.expectEqual('http://127.0.0.1:9582/over/9000', l2.href);
|
||||
|
||||
l2.crossOrigin = 'nope';
|
||||
testing.expectEqual('anonymous', l2.crossOrigin);
|
||||
|
||||
l2.crossOrigin = 'use-Credentials';
|
||||
testing.expectEqual('use-credentials', l2.crossOrigin);
|
||||
|
||||
l2.crossOrigin = '';
|
||||
testing.expectEqual('anonymous', l2.crossOrigin);
|
||||
</script>
|
||||
|
||||
@@ -50,6 +50,50 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="play_pause_events">
|
||||
{
|
||||
const audio = document.createElement('audio');
|
||||
const events = [];
|
||||
|
||||
audio.addEventListener('play', () => events.push('play'));
|
||||
audio.addEventListener('playing', () => events.push('playing'));
|
||||
audio.addEventListener('pause', () => events.push('pause'));
|
||||
audio.addEventListener('emptied', () => events.push('emptied'));
|
||||
|
||||
// First play: paused -> playing, fires play + playing
|
||||
audio.play();
|
||||
testing.expectEqual('play,playing', events.join(','));
|
||||
|
||||
// Second play: already playing, no events
|
||||
audio.play();
|
||||
testing.expectEqual('play,playing', events.join(','));
|
||||
|
||||
// Pause: playing -> paused, fires pause
|
||||
audio.pause();
|
||||
testing.expectEqual('play,playing,pause', events.join(','));
|
||||
|
||||
// Second pause: already paused, no event
|
||||
audio.pause();
|
||||
testing.expectEqual('play,playing,pause', events.join(','));
|
||||
|
||||
// Third play: resume from pause, fires play + playing (verified in Chrome)
|
||||
audio.play();
|
||||
testing.expectEqual('play,playing,pause,play,playing', events.join(','));
|
||||
|
||||
// Pause again
|
||||
audio.pause();
|
||||
testing.expectEqual('play,playing,pause,play,playing,pause', events.join(','));
|
||||
|
||||
// Load: resets state, fires emptied
|
||||
audio.load();
|
||||
testing.expectEqual('play,playing,pause,play,playing,pause,emptied', events.join(','));
|
||||
|
||||
// Play after load: fires play + playing
|
||||
audio.play();
|
||||
testing.expectEqual('play,playing,pause,play,playing,pause,emptied,play,playing', events.join(','));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="volume_muted">
|
||||
{
|
||||
const audio = document.getElementById('audio1');
|
||||
|
||||
51
src/browser/tests/element/html/ol.html
Normal file
51
src/browser/tests/element/html/ol.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<ol id="ol1" start="5" reversed type="a">
|
||||
<li>Item</li>
|
||||
</ol>
|
||||
<ol id="ol2">
|
||||
<li>Item</li>
|
||||
</ol>
|
||||
|
||||
<script id="start">
|
||||
{
|
||||
const ol1 = document.getElementById('ol1');
|
||||
testing.expectEqual(5, ol1.start);
|
||||
|
||||
ol1.start = 10;
|
||||
testing.expectEqual(10, ol1.start);
|
||||
|
||||
const ol2 = document.getElementById('ol2');
|
||||
testing.expectEqual(1, ol2.start);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="reversed">
|
||||
{
|
||||
const ol1 = document.getElementById('ol1');
|
||||
testing.expectEqual(true, ol1.reversed);
|
||||
|
||||
ol1.reversed = false;
|
||||
testing.expectEqual(false, ol1.reversed);
|
||||
|
||||
const ol2 = document.getElementById('ol2');
|
||||
testing.expectEqual(false, ol2.reversed);
|
||||
|
||||
ol2.reversed = true;
|
||||
testing.expectEqual(true, ol2.reversed);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="type">
|
||||
{
|
||||
const ol1 = document.getElementById('ol1');
|
||||
testing.expectEqual('a', ol1.type);
|
||||
|
||||
ol1.type = '1';
|
||||
testing.expectEqual('1', ol1.type);
|
||||
|
||||
const ol2 = document.getElementById('ol2');
|
||||
testing.expectEqual('1', ol2.type);
|
||||
}
|
||||
</script>
|
||||
40
src/browser/tests/element/html/optgroup.html
Normal file
40
src/browser/tests/element/html/optgroup.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<select>
|
||||
<optgroup id="og1" label="Group 1" disabled>
|
||||
<option>A</option>
|
||||
</optgroup>
|
||||
<optgroup id="og2" label="Group 2">
|
||||
<option>B</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
|
||||
<script id="disabled">
|
||||
{
|
||||
const og1 = document.getElementById('og1');
|
||||
testing.expectEqual(true, og1.disabled);
|
||||
|
||||
og1.disabled = false;
|
||||
testing.expectEqual(false, og1.disabled);
|
||||
|
||||
const og2 = document.getElementById('og2');
|
||||
testing.expectEqual(false, og2.disabled);
|
||||
|
||||
og2.disabled = true;
|
||||
testing.expectEqual(true, og2.disabled);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="label">
|
||||
{
|
||||
const og1 = document.getElementById('og1');
|
||||
testing.expectEqual('Group 1', og1.label);
|
||||
|
||||
og1.label = 'Updated';
|
||||
testing.expectEqual('Updated', og1.label);
|
||||
|
||||
const og = document.createElement('optgroup');
|
||||
testing.expectEqual('', og.label);
|
||||
}
|
||||
</script>
|
||||
17
src/browser/tests/element/html/quote.html
Normal file
17
src/browser/tests/element/html/quote.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<blockquote id="q1" cite="https://example.com/source">Quote</blockquote>
|
||||
|
||||
<script id="cite">
|
||||
{
|
||||
const q1 = document.getElementById('q1');
|
||||
testing.expectEqual('https://example.com/source', q1.cite);
|
||||
|
||||
q1.cite = 'https://example.com/other';
|
||||
testing.expectEqual('https://example.com/other', q1.cite);
|
||||
|
||||
const q2 = document.createElement('blockquote');
|
||||
testing.expectEqual('', q2.cite);
|
||||
}
|
||||
</script>
|
||||
@@ -3,7 +3,46 @@
|
||||
|
||||
<script id="sheet">
|
||||
{
|
||||
// Disconnected style element should have no sheet
|
||||
testing.expectEqual(null, document.createElement('style').sheet);
|
||||
|
||||
// Connected style element should have a CSSStyleSheet
|
||||
const style = document.createElement('style');
|
||||
document.head.appendChild(style);
|
||||
testing.expectEqual(true, style.sheet instanceof CSSStyleSheet);
|
||||
|
||||
// Same sheet instance on repeated access
|
||||
testing.expectEqual(true, style.sheet === style.sheet);
|
||||
|
||||
// Non-CSS type should have no sheet
|
||||
const lessStyle = document.createElement('style');
|
||||
lessStyle.type = 'text/less';
|
||||
document.head.appendChild(lessStyle);
|
||||
testing.expectEqual(null, lessStyle.sheet);
|
||||
|
||||
// Empty type attribute is valid (defaults to text/css per spec)
|
||||
const emptyType = document.createElement('style');
|
||||
emptyType.setAttribute('type', '');
|
||||
document.head.appendChild(emptyType);
|
||||
testing.expectEqual(true, emptyType.sheet instanceof CSSStyleSheet);
|
||||
|
||||
// Case-insensitive type check
|
||||
const upperType = document.createElement('style');
|
||||
upperType.type = 'TEXT/CSS';
|
||||
document.head.appendChild(upperType);
|
||||
testing.expectEqual(true, upperType.sheet instanceof CSSStyleSheet);
|
||||
|
||||
// Disconnection clears sheet
|
||||
const tempStyle = document.createElement('style');
|
||||
document.head.appendChild(tempStyle);
|
||||
testing.expectEqual(true, tempStyle.sheet instanceof CSSStyleSheet);
|
||||
document.head.removeChild(tempStyle);
|
||||
testing.expectEqual(null, tempStyle.sheet);
|
||||
|
||||
// ownerNode points back to the style element
|
||||
const ownStyle = document.createElement('style');
|
||||
document.head.appendChild(ownStyle);
|
||||
testing.expectEqual(true, ownStyle.sheet.ownerNode === ownStyle);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
51
src/browser/tests/element/html/tablecell.html
Normal file
51
src/browser/tests/element/html/tablecell.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td id="td1" colspan="3" rowspan="2">Cell</td>
|
||||
<td id="td2">Cell</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<script id="colSpan">
|
||||
{
|
||||
const td1 = document.getElementById('td1');
|
||||
testing.expectEqual(3, td1.colSpan);
|
||||
|
||||
td1.colSpan = 5;
|
||||
testing.expectEqual(5, td1.colSpan);
|
||||
|
||||
const td2 = document.getElementById('td2');
|
||||
testing.expectEqual(1, td2.colSpan);
|
||||
|
||||
// colSpan 0 clamps to 1
|
||||
td2.colSpan = 0;
|
||||
testing.expectEqual(1, td2.colSpan);
|
||||
|
||||
// colSpan > 1000 clamps to 1000
|
||||
td2.colSpan = 9999;
|
||||
testing.expectEqual(1000, td2.colSpan);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="rowSpan">
|
||||
{
|
||||
const td1 = document.getElementById('td1');
|
||||
testing.expectEqual(2, td1.rowSpan);
|
||||
|
||||
td1.rowSpan = 4;
|
||||
testing.expectEqual(4, td1.rowSpan);
|
||||
|
||||
const td2 = document.getElementById('td2');
|
||||
testing.expectEqual(1, td2.rowSpan);
|
||||
|
||||
// rowSpan 0 is valid per spec (span remaining rows)
|
||||
td2.rowSpan = 0;
|
||||
testing.expectEqual(0, td2.rowSpan);
|
||||
|
||||
// rowSpan > 65534 clamps to 65534
|
||||
td2.rowSpan = 99999;
|
||||
testing.expectEqual(65534, td2.rowSpan);
|
||||
}
|
||||
</script>
|
||||
17
src/browser/tests/element/html/time.html
Normal file
17
src/browser/tests/element/html/time.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<time id="t1" datetime="2024-01-15">January 15</time>
|
||||
|
||||
<script id="dateTime">
|
||||
{
|
||||
const t = document.getElementById('t1');
|
||||
testing.expectEqual('2024-01-15', t.dateTime);
|
||||
|
||||
t.dateTime = '2024-12-25T10:00';
|
||||
testing.expectEqual('2024-12-25T10:00', t.dateTime);
|
||||
|
||||
const t2 = document.createElement('time');
|
||||
testing.expectEqual('', t2.dateTime);
|
||||
}
|
||||
</script>
|
||||
116
src/browser/tests/element/position.html
Normal file
116
src/browser/tests/element/position.html
Normal file
@@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<div id="test1">Test Element</div>
|
||||
<div id="test2">Another Element</div>
|
||||
|
||||
<script id="clientDimensions">
|
||||
{
|
||||
const test1 = $('#test1');
|
||||
|
||||
// clientWidth/Height - default is 5px in dummy layout
|
||||
testing.expectEqual('number', typeof test1.clientWidth);
|
||||
testing.expectEqual('number', typeof test1.clientHeight);
|
||||
testing.expectTrue(test1.clientWidth >= 0);
|
||||
testing.expectTrue(test1.clientHeight >= 0);
|
||||
|
||||
// clientTop/Left should be 0 (no borders in dummy layout)
|
||||
testing.expectEqual(0, test1.clientTop);
|
||||
testing.expectEqual(0, test1.clientLeft);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="scrollDimensions">
|
||||
{
|
||||
const test1 = $('#test1');
|
||||
|
||||
// In dummy layout, scroll dimensions equal client dimensions (no overflow)
|
||||
testing.expectEqual(test1.clientWidth, test1.scrollWidth);
|
||||
testing.expectEqual(test1.clientHeight, test1.scrollHeight);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="scrollPosition">
|
||||
{
|
||||
const test1 = $('#test1');
|
||||
|
||||
// Initial scroll position should be 0
|
||||
testing.expectEqual(0, test1.scrollTop);
|
||||
testing.expectEqual(0, test1.scrollLeft);
|
||||
|
||||
// Setting scroll position
|
||||
test1.scrollTop = 50;
|
||||
testing.expectEqual(50, test1.scrollTop);
|
||||
|
||||
test1.scrollLeft = 25;
|
||||
testing.expectEqual(25, test1.scrollLeft);
|
||||
|
||||
// Negative values should be clamped to 0
|
||||
test1.scrollTop = -10;
|
||||
testing.expectEqual(0, test1.scrollTop);
|
||||
|
||||
test1.scrollLeft = -5;
|
||||
testing.expectEqual(0, test1.scrollLeft);
|
||||
|
||||
// Each element has independent scroll position
|
||||
const test2 = $('#test2');
|
||||
testing.expectEqual(0, test2.scrollTop);
|
||||
testing.expectEqual(0, test2.scrollLeft);
|
||||
|
||||
test2.scrollTop = 100;
|
||||
testing.expectEqual(100, test2.scrollTop);
|
||||
testing.expectEqual(0, test1.scrollTop); // test1 should still be 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="offsetDimensions">
|
||||
{
|
||||
const test1 = $('#test1');
|
||||
|
||||
// offsetWidth/Height should be numbers
|
||||
testing.expectEqual('number', typeof test1.offsetWidth);
|
||||
testing.expectEqual('number', typeof test1.offsetHeight);
|
||||
testing.expectTrue(test1.offsetWidth >= 0);
|
||||
testing.expectTrue(test1.offsetHeight >= 0);
|
||||
|
||||
// Should equal client dimensions
|
||||
testing.expectEqual(test1.clientWidth, test1.offsetWidth);
|
||||
testing.expectEqual(test1.clientHeight, test1.offsetHeight);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="offsetPosition">
|
||||
{
|
||||
const test1 = $('#test1');
|
||||
const test2 = $('#test2');
|
||||
|
||||
// offsetTop/Left should be calculated from tree position
|
||||
// These values are based on the heuristic layout engine
|
||||
const top1 = test1.offsetTop;
|
||||
const left1 = test1.offsetLeft;
|
||||
const top2 = test2.offsetTop;
|
||||
const left2 = test2.offsetLeft;
|
||||
|
||||
// Position values should be numbers
|
||||
testing.expectEqual('number', typeof top1);
|
||||
testing.expectEqual('number', typeof left1);
|
||||
testing.expectEqual('number', typeof top2);
|
||||
testing.expectEqual('number', typeof left2);
|
||||
|
||||
// Siblings should have different positions (either different x or y)
|
||||
testing.expectTrue(top1 !== top2 || left1 !== left2);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="offsetVsBounding">
|
||||
{
|
||||
const test1 = $('#test1');
|
||||
|
||||
// offsetTop/Left should match getBoundingClientRect
|
||||
const rect = test1.getBoundingClientRect();
|
||||
testing.expectEqual(rect.y, test1.offsetTop);
|
||||
testing.expectEqual(rect.x, test1.offsetLeft);
|
||||
testing.expectEqual(rect.width, test1.offsetWidth);
|
||||
testing.expectEqual(rect.height, test1.offsetHeight);
|
||||
}
|
||||
</script>
|
||||
22
src/browser/tests/event/focus.html
Normal file
22
src/browser/tests/event/focus.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<script id=default>
|
||||
let event = new FocusEvent('focus');
|
||||
testing.expectEqual('focus', event.type);
|
||||
testing.expectEqual(true, event instanceof FocusEvent);
|
||||
testing.expectEqual(true, event instanceof UIEvent);
|
||||
testing.expectEqual(true, event instanceof Event);
|
||||
testing.expectEqual(null, event.relatedTarget);
|
||||
</script>
|
||||
|
||||
<script id=parameters>
|
||||
let div = document.createElement('div');
|
||||
let focusEvent = new FocusEvent('blur', { relatedTarget: div });
|
||||
testing.expectEqual(div, focusEvent.relatedTarget);
|
||||
</script>
|
||||
|
||||
<script id=createEvent>
|
||||
let evt = document.createEvent('focusevent');
|
||||
testing.expectEqual(true, evt instanceof FocusEvent);
|
||||
testing.expectEqual(true, evt instanceof UIEvent);
|
||||
</script>
|
||||
@@ -10,11 +10,13 @@
|
||||
testing.expectEqual(0, event.clientY);
|
||||
testing.expectEqual(0, event.screenX);
|
||||
testing.expectEqual(0, event.screenY);
|
||||
testing.expectEqual(0, event.buttons);
|
||||
</script>
|
||||
|
||||
<script id=parameters>
|
||||
let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20, screenX: 200, screenY: 500 });
|
||||
let new_event = new MouseEvent('click', { 'button': 0, 'clientX': 10, 'clientY': 20, screenX: 200, screenY: 500, buttons: 5 });
|
||||
testing.expectEqual(0, new_event.button);
|
||||
testing.expectEqual(5, new_event.buttons);
|
||||
testing.expectEqual(10, new_event.x);
|
||||
testing.expectEqual(20, new_event.y);
|
||||
testing.expectEqual(10, new_event.pageX);
|
||||
|
||||
17
src/browser/tests/event/text.html
Normal file
17
src/browser/tests/event/text.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<script id=createEvent>
|
||||
let evt = document.createEvent('TextEvent');
|
||||
testing.expectEqual(true, evt instanceof TextEvent);
|
||||
testing.expectEqual(true, evt instanceof UIEvent);
|
||||
testing.expectEqual('', evt.data);
|
||||
</script>
|
||||
|
||||
<script id=initTextEvent>
|
||||
let textEvent = document.createEvent('TextEvent');
|
||||
textEvent.initTextEvent('textInput', true, false, window, 'test data');
|
||||
testing.expectEqual('textInput', textEvent.type);
|
||||
testing.expectEqual('test data', textEvent.data);
|
||||
testing.expectEqual(true, textEvent.bubbles);
|
||||
testing.expectEqual(false, textEvent.cancelable);
|
||||
</script>
|
||||
30
src/browser/tests/event/wheel.html
Normal file
30
src/browser/tests/event/wheel.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
<script id=default>
|
||||
let event = new WheelEvent('wheel');
|
||||
testing.expectEqual('wheel', event.type);
|
||||
testing.expectEqual(true, event instanceof WheelEvent);
|
||||
testing.expectEqual(true, event instanceof MouseEvent);
|
||||
testing.expectEqual(true, event instanceof UIEvent);
|
||||
testing.expectEqual(0, event.deltaX);
|
||||
testing.expectEqual(0, event.deltaY);
|
||||
testing.expectEqual(0, event.deltaZ);
|
||||
testing.expectEqual(0, event.deltaMode);
|
||||
</script>
|
||||
|
||||
<script id=parameters>
|
||||
let wheelEvent = new WheelEvent('wheel', {
|
||||
deltaX: 10,
|
||||
deltaY: 20,
|
||||
deltaMode: WheelEvent.DOM_DELTA_LINE
|
||||
});
|
||||
testing.expectEqual(10, wheelEvent.deltaX);
|
||||
testing.expectEqual(20, wheelEvent.deltaY);
|
||||
testing.expectEqual(1, wheelEvent.deltaMode);
|
||||
</script>
|
||||
|
||||
<script id=constants>
|
||||
testing.expectEqual(0, WheelEvent.DOM_DELTA_PIXEL);
|
||||
testing.expectEqual(1, WheelEvent.DOM_DELTA_LINE);
|
||||
testing.expectEqual(2, WheelEvent.DOM_DELTA_PAGE);
|
||||
</script>
|
||||
@@ -635,3 +635,130 @@
|
||||
// https://github.com/lightpanda-io/browser/pull/1316
|
||||
testing.expectError('TypeError', () => MessageEvent(''));
|
||||
</script>
|
||||
|
||||
<div id=inline_parent><div id=inline_child></div></div>
|
||||
<script id=inlineHandlerReceivesEvent>
|
||||
// Test that inline onclick handler receives the event object
|
||||
{
|
||||
const inline_child = $('#inline_child');
|
||||
let receivedType = null;
|
||||
let receivedTarget = null;
|
||||
let receivedCurrentTarget = null;
|
||||
|
||||
inline_child.onclick = function(e) {
|
||||
// Capture values DURING handler execution
|
||||
receivedType = e.type;
|
||||
receivedTarget = e.target;
|
||||
receivedCurrentTarget = e.currentTarget;
|
||||
};
|
||||
|
||||
inline_child.click();
|
||||
|
||||
testing.expectEqual('click', receivedType);
|
||||
testing.expectEqual(inline_child, receivedTarget);
|
||||
testing.expectEqual(inline_child, receivedCurrentTarget);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=inline_order_parent><div id=inline_order_child></div></div>
|
||||
<script id=inlineHandlerOrder>
|
||||
// Test that inline handler executes in proper order with addEventListener
|
||||
{
|
||||
const inline_order_child = $('#inline_order_child');
|
||||
const inline_order_parent = $('#inline_order_parent');
|
||||
const order = [];
|
||||
|
||||
// Capture listener on parent
|
||||
inline_order_parent.addEventListener('click', () => order.push('parent-capture'), true);
|
||||
|
||||
// Inline handler on child (should execute at target phase)
|
||||
inline_order_child.onclick = () => order.push('child-onclick');
|
||||
|
||||
// addEventListener on child (should execute at target phase, after onclick)
|
||||
inline_order_child.addEventListener('click', () => order.push('child-listener'));
|
||||
|
||||
// Bubble listener on parent
|
||||
inline_order_parent.addEventListener('click', () => order.push('parent-bubble'));
|
||||
|
||||
inline_order_child.click();
|
||||
|
||||
// Expected order: capture, then onclick, then addEventListener, then bubble
|
||||
testing.expectEqual('parent-capture', order[0]);
|
||||
testing.expectEqual('child-onclick', order[1]);
|
||||
testing.expectEqual('child-listener', order[2]);
|
||||
testing.expectEqual('parent-bubble', order[3]);
|
||||
testing.expectEqual(4, order.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=inline_prevent><div id=inline_prevent_child></div></div>
|
||||
<script id=inlineHandlerPreventDefault>
|
||||
// Test that inline handler can preventDefault and it affects addEventListener listeners
|
||||
{
|
||||
const inline_prevent_child = $('#inline_prevent_child');
|
||||
let preventDefaultCalled = false;
|
||||
let listenerSawPrevented = false;
|
||||
|
||||
inline_prevent_child.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
preventDefaultCalled = true;
|
||||
};
|
||||
|
||||
inline_prevent_child.addEventListener('click', (e) => {
|
||||
listenerSawPrevented = e.defaultPrevented;
|
||||
});
|
||||
|
||||
const result = inline_prevent_child.dispatchEvent(new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}));
|
||||
|
||||
testing.expectEqual(true, preventDefaultCalled);
|
||||
testing.expectEqual(true, listenerSawPrevented);
|
||||
testing.expectEqual(false, result); // dispatchEvent returns false when prevented
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=inline_stop_parent><div id=inline_stop_child></div></div>
|
||||
<script id=inlineHandlerStopPropagation>
|
||||
// Test that inline handler can stopPropagation
|
||||
{
|
||||
const inline_stop_child = $('#inline_stop_child');
|
||||
const inline_stop_parent = $('#inline_stop_parent');
|
||||
let childCalled = false;
|
||||
let parentCalled = false;
|
||||
|
||||
inline_stop_child.onclick = function(e) {
|
||||
childCalled = true;
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
inline_stop_parent.addEventListener('click', () => {
|
||||
parentCalled = true;
|
||||
});
|
||||
|
||||
inline_stop_child.click();
|
||||
|
||||
testing.expectEqual(true, childCalled);
|
||||
testing.expectEqual(false, parentCalled); // Should not bubble to parent
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=inline_replace_test></div>
|
||||
<script id=inlineHandlerReplacement>
|
||||
// Test that setting onclick property replaces previous handler
|
||||
{
|
||||
const inline_replace_test = $('#inline_replace_test');
|
||||
let calls = [];
|
||||
|
||||
inline_replace_test.onclick = () => calls.push('first');
|
||||
inline_replace_test.click();
|
||||
|
||||
inline_replace_test.onclick = () => calls.push('second');
|
||||
inline_replace_test.click();
|
||||
|
||||
testing.expectEqual('first', calls[0]);
|
||||
testing.expectEqual('second', calls[1]);
|
||||
testing.expectEqual(2, calls.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
75
src/browser/tests/image_data.html
Normal file
75
src/browser/tests/image_data.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="testing.js"></script>
|
||||
|
||||
<script id=constructor-basic>
|
||||
{
|
||||
const img = new ImageData(10, 20);
|
||||
testing.expectEqual(10, img.width);
|
||||
testing.expectEqual(20, img.height);
|
||||
testing.expectEqual("srgb", img.colorSpace);
|
||||
testing.expectEqual("rgba-unorm8", img.pixelFormat);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=data-property>
|
||||
{
|
||||
const img = new ImageData(2, 3);
|
||||
const data = img.data;
|
||||
testing.expectEqual(true, data instanceof Uint8ClampedArray);
|
||||
// 2 * 3 * 4 (RGBA) = 24 bytes
|
||||
testing.expectEqual(24, data.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=data-initialized-to-zero>
|
||||
{
|
||||
const img = new ImageData(2, 2);
|
||||
const data = img.data;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
testing.expectEqual(0, data[i]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=data-mutability>
|
||||
{
|
||||
const img = new ImageData(1, 1);
|
||||
const data = img.data;
|
||||
// Set pixel to red (RGBA)
|
||||
data[0] = 255;
|
||||
data[1] = 0;
|
||||
data[2] = 0;
|
||||
data[3] = 255;
|
||||
|
||||
// Read back through the same accessor
|
||||
const data2 = img.data;
|
||||
testing.expectEqual(255, data2[0]);
|
||||
testing.expectEqual(0, data2[1]);
|
||||
testing.expectEqual(0, data2[2]);
|
||||
testing.expectEqual(255, data2[3]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=constructor-with-settings>
|
||||
{
|
||||
const img = new ImageData(5, 5, { colorSpace: "srgb" });
|
||||
testing.expectEqual(5, img.width);
|
||||
testing.expectEqual(5, img.height);
|
||||
testing.expectEqual("srgb", img.colorSpace);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=constructor-invalid-colorspace>
|
||||
testing.expectError("TypeError", () => {
|
||||
new ImageData(5, 5, { colorSpace: "display-p3" });
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=single-pixel>
|
||||
{
|
||||
const img = new ImageData(1, 1);
|
||||
testing.expectEqual(4, img.data.length);
|
||||
testing.expectEqual(1, img.width);
|
||||
testing.expectEqual(1, img.height);
|
||||
}
|
||||
</script>
|
||||
@@ -25,6 +25,8 @@
|
||||
testing.expectEqual('number', typeof entry.intersectionRatio);
|
||||
testing.expectEqual('object', typeof entry.boundingClientRect);
|
||||
testing.expectEqual('object', typeof entry.intersectionRect);
|
||||
testing.expectEqual('number', typeof entry.time);
|
||||
testing.expectEqual(true, entry.time > 0);
|
||||
|
||||
observer.disconnect();
|
||||
});
|
||||
|
||||
@@ -222,3 +222,33 @@
|
||||
testing.expectEqual(XMLHttpRequest.UNSENT, req.readyState);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=xhr_abort_callback>
|
||||
testing.async(async (restore) => {
|
||||
const req = new XMLHttpRequest();
|
||||
let abortFired = false;
|
||||
let errorFired = false;
|
||||
let loadEndFired = false;
|
||||
|
||||
await new Promise((resolve) => {
|
||||
req.onabort = () => { abortFired = true; };
|
||||
req.onerror = () => { errorFired = true; };
|
||||
req.onloadend = () => {
|
||||
loadEndFired = true;
|
||||
resolve();
|
||||
};
|
||||
|
||||
req.open('GET', 'http://127.0.0.1:9582/xhr');
|
||||
req.onreadystatechange = (e) => {
|
||||
req.abort();
|
||||
}
|
||||
req.send();
|
||||
});
|
||||
|
||||
restore();
|
||||
testing.expectEqual(true, abortFired);
|
||||
testing.expectEqual(true, errorFired);
|
||||
testing.expectEqual(true, loadEndFired);
|
||||
testing.expectEqual(XMLHttpRequest.UNSENT, req.readyState);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
testing.expectEqual(undefined, children[-1]);
|
||||
|
||||
testing.expectEqual(['p1', 'p2'], Array.from(children).map((n) => n.id));
|
||||
|
||||
testing.expectEqual(false, 10 in children);
|
||||
</script>
|
||||
|
||||
<script id=values>
|
||||
|
||||
@@ -210,3 +210,28 @@
|
||||
testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode().__proto__.constructor.name);
|
||||
testing.expectEqual('HTMLDivElement', disconnectedChild.getRootNode({ composed: true }).__proto__.constructor.name);
|
||||
</script>
|
||||
|
||||
<div id=contains><div id=other></div></div>
|
||||
<script id=contains>
|
||||
{
|
||||
const d1 = $('#contains');
|
||||
const d2 = $('#other');
|
||||
testing.expectEqual(false, d1.contains(null));
|
||||
testing.expectEqual(true, d1.contains(d1));
|
||||
testing.expectEqual(false, d2.contains(d1));
|
||||
testing.expectEqual(true, d1.contains(d2));
|
||||
testing.expectEqual(false, d1.contains(p1));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=childNodes>
|
||||
{
|
||||
const d1 = $('#contains');
|
||||
testing.expectEqual(true, d1.childNodes === d1.childNodes)
|
||||
|
||||
let c1 = d1.childNodes;
|
||||
d1.removeChild(c1[0])
|
||||
testing.expectEqual(0, c1.length);
|
||||
testing.expectEqual(0, d1.childNodes.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<<<<<<< HEAD
|
||||
<script id="microtask_access_to_list">
|
||||
{
|
||||
|
||||
@@ -69,3 +68,31 @@
|
||||
<script>
|
||||
testing.expectEqual(['mark', 'measure'], PerformanceObserver.supportedEntryTypes);
|
||||
</script>
|
||||
|
||||
<script id="buffered_option">
|
||||
{
|
||||
// Clear marks from previous tests so we get a precise count
|
||||
performance.clearMarks();
|
||||
|
||||
// Create marks BEFORE the observer
|
||||
performance.mark("early1", { startTime: 1.0 });
|
||||
performance.mark("early2", { startTime: 2.0 });
|
||||
|
||||
let receivedEntries = null;
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
receivedEntries = list.getEntries();
|
||||
});
|
||||
|
||||
// With buffered: true, existing marks should be delivered
|
||||
observer.observe({ type: "mark", buffered: true });
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual(true, receivedEntries !== null);
|
||||
testing.expectEqual(2, receivedEntries.length);
|
||||
testing.expectEqual("early1", receivedEntries[0].name);
|
||||
testing.expectEqual("early2", receivedEntries[1].name);
|
||||
|
||||
observer.disconnect();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -191,6 +191,74 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=toString_sameText>
|
||||
{
|
||||
const p = document.createElement('p');
|
||||
p.textContent = 'Hello World';
|
||||
|
||||
const range = document.createRange();
|
||||
range.setStart(p.firstChild, 3);
|
||||
range.setEnd(p.firstChild, 8);
|
||||
|
||||
testing.expectEqual('lo Wo', range.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=toString_sameElement>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = '<p>First</p><p>Second</p><p>Third</p>';
|
||||
|
||||
const range = document.createRange();
|
||||
range.setStart(div, 0);
|
||||
range.setEnd(div, 2);
|
||||
|
||||
testing.expectEqual('FirstSecond', range.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=toString_crossContainer_siblings>
|
||||
{
|
||||
const p = document.createElement('p');
|
||||
p.appendChild(document.createTextNode('AAAA'));
|
||||
p.appendChild(document.createTextNode('BBBB'));
|
||||
p.appendChild(document.createTextNode('CCCC'));
|
||||
|
||||
const range = document.createRange();
|
||||
range.setStart(p.childNodes[0], 2);
|
||||
range.setEnd(p.childNodes[2], 2);
|
||||
|
||||
testing.expectEqual('AABBBBCC', range.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=toString_crossContainer_nested>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = '<p>First paragraph</p><p>Second paragraph</p>';
|
||||
|
||||
const range = document.createRange();
|
||||
range.setStart(div.querySelector('p').firstChild, 6);
|
||||
range.setEnd(div.querySelectorAll('p')[1].firstChild, 6);
|
||||
|
||||
testing.expectEqual('paragraphSecond', range.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=toString_excludes_comments>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode('before'));
|
||||
div.appendChild(document.createComment('this is a comment'));
|
||||
div.appendChild(document.createTextNode('after'));
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(div);
|
||||
|
||||
testing.expectEqual('beforeafter', range.toString());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=insertNode>
|
||||
{
|
||||
const range = document.createRange();
|
||||
@@ -743,11 +811,11 @@
|
||||
// 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));
|
||||
testing.expectEqual(1, range1.compareBoundaryPoints(Range.START_TO_END, range2));
|
||||
|
||||
// range1 start is before range2 end
|
||||
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));
|
||||
@@ -767,11 +835,11 @@
|
||||
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));
|
||||
testing.expectEqual(1, range.compareBoundaryPoints(Range.START_TO_END, range));
|
||||
|
||||
// Start is before end
|
||||
testing.expectEqual(-1, range.compareBoundaryPoints(Range.END_TO_START, range));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
34
src/browser/tests/window/stubs.html
Normal file
34
src/browser/tests/window/stubs.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id="alert">
|
||||
{
|
||||
// alert should be callable without error
|
||||
window.alert('hello');
|
||||
window.alert();
|
||||
testing.expectEqual(undefined, window.alert('test'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="confirm">
|
||||
{
|
||||
// confirm returns false in headless mode
|
||||
testing.expectEqual(false, window.confirm('proceed?'));
|
||||
testing.expectEqual(false, window.confirm());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="prompt">
|
||||
{
|
||||
// prompt returns null in headless mode
|
||||
testing.expectEqual(null, window.prompt('enter value'));
|
||||
testing.expectEqual(null, window.prompt('enter value', 'default'));
|
||||
testing.expectEqual(null, window.prompt());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="devicePixelRatio">
|
||||
{
|
||||
testing.expectEqual(1, window.devicePixelRatio);
|
||||
}
|
||||
</script>
|
||||
@@ -49,6 +49,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(AbortController.init, .{});
|
||||
|
||||
@@ -157,6 +157,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const Prototype = EventTarget;
|
||||
|
||||
@@ -273,6 +273,7 @@ pub const JsApi = struct {
|
||||
pub const name = "CharacterData";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const data = bridge.accessor(CData.getData, CData.setData, .{});
|
||||
|
||||
@@ -34,6 +34,7 @@ pub fn createDocumentType(_: *const DOMImplementation, qualified_name: []const u
|
||||
pub fn createHTMLDocument(_: *const DOMImplementation, title: ?[]const u8, page: *Page) !*Document {
|
||||
const document = (try page._factory.document(Node.Document.HTMLDocument{ ._proto = undefined })).asDocument();
|
||||
document._ready_state = .complete;
|
||||
document._url = "about:blank";
|
||||
|
||||
{
|
||||
const doctype = try page._factory.node(DocumentType{
|
||||
@@ -67,6 +68,7 @@ pub fn createHTMLDocument(_: *const DOMImplementation, title: ?[]const u8, page:
|
||||
pub fn createDocument(_: *const DOMImplementation, namespace_: ?[]const u8, qualified_name: ?[]const u8, doctype: ?*DocumentType, page: *Page) !*Document {
|
||||
// Create XML Document
|
||||
const document = (try page._factory.document(Node.Document.XMLDocument{ ._proto = undefined })).asDocument();
|
||||
document._url = "about:blank";
|
||||
|
||||
// Append doctype if provided
|
||||
if (doctype) |dt| {
|
||||
@@ -99,6 +101,7 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const empty_with_no_proto = true;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const createDocumentType = bridge.function(DOMImplementation.createDocumentType, .{ .dom_exception = true });
|
||||
|
||||
@@ -192,6 +192,7 @@ pub const JsApi = struct {
|
||||
pub const name = "NodeIterator";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const root = bridge.accessor(DOMNodeIterator.getRoot, null, .{});
|
||||
|
||||
@@ -344,6 +344,7 @@ pub const JsApi = struct {
|
||||
pub const name = "TreeWalker";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const root = bridge.accessor(DOMTreeWalker.getRoot, null, .{});
|
||||
|
||||
@@ -44,6 +44,7 @@ const Document = @This();
|
||||
_type: Type,
|
||||
_proto: *Node,
|
||||
_location: ?*Location = null,
|
||||
_url: ?[:0]const u8 = null, // URL for documents created via DOMImplementation (about:blank)
|
||||
_ready_state: ReadyState = .loading,
|
||||
_current_script: ?*Element.Html.Script = null,
|
||||
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .empty,
|
||||
@@ -105,8 +106,8 @@ pub fn asEventTarget(self: *Document) *@import("EventTarget.zig") {
|
||||
return self._proto.asEventTarget();
|
||||
}
|
||||
|
||||
pub fn getURL(_: *const Document, page: *const Page) [:0]const u8 {
|
||||
return page.url;
|
||||
pub fn getURL(self: *const Document, page: *const Page) [:0]const u8 {
|
||||
return self._url orelse page.url;
|
||||
}
|
||||
|
||||
pub fn getContentType(self: *const Document) []const u8 {
|
||||
@@ -131,8 +132,8 @@ pub fn createElement(self: *Document, name: []const u8, options_: ?CreateElement
|
||||
if (self._type == .html) {
|
||||
break :blk .{ .html, std.ascii.lowerString(&page.buf, name) };
|
||||
}
|
||||
// Generic and XML documents create XML elements
|
||||
break :blk .{ .xml, name };
|
||||
// Generic and XML documents create elements with null namespace
|
||||
break :blk .{ .null, name };
|
||||
};
|
||||
// HTML documents are case-insensitive - lowercase the tag name
|
||||
|
||||
@@ -218,47 +219,16 @@ pub fn getElementById(self: *Document, id: []const u8, page: *Page) ?*Element {
|
||||
return null;
|
||||
}
|
||||
|
||||
const GetElementsByTagNameResult = union(enum) {
|
||||
tag: collections.NodeLive(.tag),
|
||||
tag_name: collections.NodeLive(.tag_name),
|
||||
all_elements: collections.NodeLive(.all_elements),
|
||||
};
|
||||
pub fn getElementsByTagName(self: *Document, tag_name: []const u8, page: *Page) !GetElementsByTagNameResult {
|
||||
if (tag_name.len > 256) {
|
||||
// 256 seems generous.
|
||||
return error.InvalidTagName;
|
||||
}
|
||||
pub fn getElementsByTagName(self: *Document, tag_name: []const u8, page: *Page) !Node.GetElementsByTagNameResult {
|
||||
return self.asNode().getElementsByTagName(tag_name, page);
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, tag_name, "*")) {
|
||||
return .{
|
||||
.all_elements = collections.NodeLive(.all_elements).init(self.asNode(), {}, page),
|
||||
};
|
||||
}
|
||||
|
||||
const lower = std.ascii.lowerString(&page.buf, tag_name);
|
||||
if (Node.Element.Tag.parseForMatch(lower)) |known| {
|
||||
// optimized for known tag names, comparis
|
||||
return .{
|
||||
.tag = collections.NodeLive(.tag).init(self.asNode(), known, page),
|
||||
};
|
||||
}
|
||||
|
||||
const arena = page.arena;
|
||||
const filter = try String.init(arena, lower, .{});
|
||||
return .{ .tag_name = collections.NodeLive(.tag_name).init(self.asNode(), filter, page) };
|
||||
pub fn getElementsByTagNameNS(self: *Document, namespace: ?[]const u8, local_name: []const u8, page: *Page) !collections.NodeLive(.tag_name_ns) {
|
||||
return self.asNode().getElementsByTagNameNS(namespace, local_name, page);
|
||||
}
|
||||
|
||||
pub fn getElementsByClassName(self: *Document, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) {
|
||||
const arena = page.arena;
|
||||
|
||||
// Parse space-separated class names
|
||||
var class_names: std.ArrayList([]const u8) = .empty;
|
||||
var it = std.mem.tokenizeAny(u8, class_name, "\t\n\x0C\r ");
|
||||
while (it.next()) |name| {
|
||||
try class_names.append(arena, try page.dupeString(name));
|
||||
}
|
||||
|
||||
return collections.NodeLive(.class_name).init(self.asNode(), class_names.items, page);
|
||||
return self.asNode().getElementsByClassName(class_name, page);
|
||||
}
|
||||
|
||||
pub fn getElementsByName(self: *Document, name: []const u8, page: *Page) !collections.NodeLive(.name) {
|
||||
@@ -354,19 +324,48 @@ pub fn createRange(_: *const Document, page: *Page) !*Range {
|
||||
|
||||
pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@import("Event.zig") {
|
||||
const Event = @import("Event.zig");
|
||||
if (event_type.len > 100) {
|
||||
return error.NotSupported;
|
||||
}
|
||||
const normalized = std.ascii.lowerString(&page.buf, event_type);
|
||||
|
||||
if (std.ascii.eqlIgnoreCase(event_type, "event") or std.ascii.eqlIgnoreCase(event_type, "events") or std.ascii.eqlIgnoreCase(event_type, "htmlevents")) {
|
||||
if (std.mem.eql(u8, normalized, "event") or std.mem.eql(u8, normalized, "events") or std.mem.eql(u8, normalized, "htmlevents")) {
|
||||
return Event.init("", null, page);
|
||||
}
|
||||
|
||||
if (std.ascii.eqlIgnoreCase(event_type, "customevent") or std.ascii.eqlIgnoreCase(event_type, "customevents")) {
|
||||
if (std.mem.eql(u8, normalized, "customevent") or std.mem.eql(u8, normalized, "customevents")) {
|
||||
const CustomEvent = @import("event/CustomEvent.zig");
|
||||
const custom_event = try CustomEvent.init("", null, page);
|
||||
return custom_event.asEvent();
|
||||
return (try CustomEvent.init("", null, page)).asEvent();
|
||||
}
|
||||
|
||||
if (std.ascii.eqlIgnoreCase(event_type, "messageevent")) {
|
||||
return error.NotSupported;
|
||||
if (std.mem.eql(u8, normalized, "keyboardevent")) {
|
||||
const KeyboardEvent = @import("event/KeyboardEvent.zig");
|
||||
return (try KeyboardEvent.init("", null, page)).asEvent();
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, normalized, "mouseevent") or std.mem.eql(u8, normalized, "mouseevents")) {
|
||||
const MouseEvent = @import("event/MouseEvent.zig");
|
||||
return (try MouseEvent.init("", null, page)).asEvent();
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, normalized, "messageevent")) {
|
||||
const MessageEvent = @import("event/MessageEvent.zig");
|
||||
return (try MessageEvent.init("", null, page)).asEvent();
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, normalized, "uievent") or std.mem.eql(u8, normalized, "uievents")) {
|
||||
const UIEvent = @import("event/UIEvent.zig");
|
||||
return (try UIEvent.init("", null, page)).asEvent();
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, normalized, "focusevent") or std.mem.eql(u8, normalized, "focusevents")) {
|
||||
const FocusEvent = @import("event/FocusEvent.zig");
|
||||
return (try FocusEvent.init("", null, page)).asEvent();
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, normalized, "textevent") or std.mem.eql(u8, normalized, "textevents")) {
|
||||
const TextEvent = @import("event/TextEvent.zig");
|
||||
return (try TextEvent.init("", null, page)).asEvent();
|
||||
}
|
||||
|
||||
return error.NotSupported;
|
||||
@@ -913,7 +912,8 @@ fn validateElementName(name: []const u8) !void {
|
||||
const is_valid = (c >= 'a' and c <= 'z') or
|
||||
(c >= 'A' and c <= 'Z') or
|
||||
(c >= '0' and c <= '9') or
|
||||
c == '_' or c == '-' or c == '.' or c == ':';
|
||||
c == '_' or c == '-' or c == '.' or c == ':' or
|
||||
c >= 128; // Allow non-ASCII UTF-8
|
||||
|
||||
if (!is_valid) {
|
||||
return error.InvalidCharacterError;
|
||||
@@ -934,6 +934,7 @@ pub const JsApi = struct {
|
||||
pub const name = "Document";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(_constructor, .{});
|
||||
@@ -948,6 +949,7 @@ pub const JsApi = struct {
|
||||
pub const URL = bridge.accessor(Document.getURL, null, .{});
|
||||
pub const documentURI = bridge.accessor(Document.getURL, null, .{});
|
||||
pub const documentElement = bridge.accessor(Document.getDocumentElement, null, .{});
|
||||
pub const scrollingElement = bridge.accessor(Document.getDocumentElement, null, .{});
|
||||
pub const children = bridge.accessor(Document.getChildren, null, .{});
|
||||
pub const readyState = bridge.accessor(Document.getReadyState, null, .{});
|
||||
pub const implementation = bridge.accessor(Document.getImplementation, null, .{});
|
||||
@@ -982,6 +984,7 @@ pub const JsApi = struct {
|
||||
pub const querySelector = bridge.function(Document.querySelector, .{ .dom_exception = true });
|
||||
pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true });
|
||||
pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{});
|
||||
pub const getElementsByTagNameNS = bridge.function(Document.getElementsByTagNameNS, .{});
|
||||
pub const getSelection = bridge.function(Document.getSelection, .{});
|
||||
pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{});
|
||||
pub const getElementsByName = bridge.function(Document.getElementsByName, .{});
|
||||
|
||||
@@ -233,6 +233,7 @@ pub const JsApi = struct {
|
||||
pub const name = "DocumentFragment";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(DocumentFragment.init, .{});
|
||||
|
||||
@@ -81,6 +81,7 @@ pub const JsApi = struct {
|
||||
pub const name = "DocumentType";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const name = bridge.accessor(DocumentType.getName, null, .{});
|
||||
|
||||
@@ -49,6 +49,12 @@ pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTok
|
||||
pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot);
|
||||
pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot);
|
||||
|
||||
pub const ScrollPosition = struct {
|
||||
x: u32 = 0,
|
||||
y: u32 = 0,
|
||||
};
|
||||
pub const ScrollPositionLookup = std.AutoHashMapUnmanaged(*Element, ScrollPosition);
|
||||
|
||||
pub const Namespace = enum(u8) {
|
||||
html,
|
||||
svg,
|
||||
@@ -204,6 +210,7 @@ pub fn getTagNameLower(self: *const Element) []const u8 {
|
||||
.dialog => "dialog",
|
||||
.directory => "dir",
|
||||
.div => "div",
|
||||
.dl => "dl",
|
||||
.embed => "embed",
|
||||
.fieldset => "fieldset",
|
||||
.font => "font",
|
||||
@@ -281,6 +288,7 @@ pub fn getTagNameSpec(self: *const Element, buf: []u8) []const u8 {
|
||||
.dialog => "DIALOG",
|
||||
.directory => "DIR",
|
||||
.div => "DIV",
|
||||
.dl => "DL",
|
||||
.embed => "EMBED",
|
||||
.fieldset => "FIELDSET",
|
||||
.font => "FONT",
|
||||
@@ -674,6 +682,11 @@ pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList {
|
||||
return gop.value_ptr.*;
|
||||
}
|
||||
|
||||
pub fn setClassList(self: *Element, value: String, page: *Page) !void {
|
||||
const class_list = try self.getClassList(page);
|
||||
try class_list.setValue(value, page);
|
||||
}
|
||||
|
||||
pub fn getRelList(self: *Element, page: *Page) !*collections.DOMTokenList {
|
||||
const gop = try page._element_rel_lists.getOrPut(page.arena, self);
|
||||
if (!gop.found_existing) {
|
||||
@@ -762,25 +775,45 @@ pub fn remove(self: *Element, page: *Page) void {
|
||||
}
|
||||
|
||||
pub fn focus(self: *Element, page: *Page) !void {
|
||||
const Event = @import("Event.zig");
|
||||
const FocusEvent = @import("event/FocusEvent.zig");
|
||||
|
||||
// Capture relatedTarget before anything changes
|
||||
const old_related: ?*@import("EventTarget.zig") = if (page.document._active_element) |old| old.asEventTarget() else null;
|
||||
const new_target = self.asEventTarget();
|
||||
|
||||
if (page.document._active_element) |old| {
|
||||
if (old == self) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blur_event = try Event.initTrusted(comptime .wrap("blur"), null, page);
|
||||
defer if (!blur_event._v8_handoff) blur_event.deinit(false);
|
||||
try page._event_manager.dispatch(old.asEventTarget(), blur_event);
|
||||
const old_target = old.asEventTarget();
|
||||
|
||||
// Dispatch blur on old element (no bubble, composed)
|
||||
const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true, .relatedTarget = new_target }, page);
|
||||
defer if (!blur_event.asEvent()._v8_handoff) blur_event.deinit(false);
|
||||
try page._event_manager.dispatch(old_target, blur_event.asEvent());
|
||||
|
||||
// Dispatch focusout on old element (bubbles, composed)
|
||||
const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true, .relatedTarget = new_target }, page);
|
||||
defer if (!focusout_event.asEvent()._v8_handoff) focusout_event.deinit(false);
|
||||
try page._event_manager.dispatch(old_target, focusout_event.asEvent());
|
||||
}
|
||||
|
||||
// Must be set after blur/focusout and before focus/focusin —
|
||||
// event dispatch can reset _active_element if set earlier.
|
||||
if (self.asNode().isConnected()) {
|
||||
page.document._active_element = self;
|
||||
}
|
||||
|
||||
const focus_event = try Event.initTrusted(comptime .wrap("focus"), null, page);
|
||||
defer if (!focus_event._v8_handoff) focus_event.deinit(false);
|
||||
try page._event_manager.dispatch(self.asEventTarget(), focus_event);
|
||||
// Dispatch focus on new element (no bubble, composed)
|
||||
const focus_event = try FocusEvent.initTrusted(comptime .wrap("focus"), .{ .composed = true, .relatedTarget = old_related }, page);
|
||||
defer if (!focus_event.asEvent()._v8_handoff) focus_event.deinit(false);
|
||||
try page._event_manager.dispatch(new_target, focus_event.asEvent());
|
||||
|
||||
// Dispatch focusin on new element (bubbles, composed)
|
||||
const focusin_event = try FocusEvent.initTrusted(comptime .wrap("focusin"), .{ .bubbles = true, .composed = true, .relatedTarget = old_related }, page);
|
||||
defer if (!focusin_event.asEvent()._v8_handoff) focusin_event.deinit(false);
|
||||
try page._event_manager.dispatch(new_target, focusin_event.asEvent());
|
||||
}
|
||||
|
||||
pub fn blur(self: *Element, page: *Page) !void {
|
||||
@@ -788,10 +821,18 @@ pub fn blur(self: *Element, page: *Page) !void {
|
||||
|
||||
page.document._active_element = null;
|
||||
|
||||
const Event = @import("Event.zig");
|
||||
const blur_event = try Event.initTrusted(comptime .wrap("blur"), null, page);
|
||||
defer if (!blur_event._v8_handoff) blur_event.deinit(false);
|
||||
try page._event_manager.dispatch(self.asEventTarget(), blur_event);
|
||||
const FocusEvent = @import("event/FocusEvent.zig");
|
||||
const old_target = self.asEventTarget();
|
||||
|
||||
// Dispatch blur (no bubble, composed)
|
||||
const blur_event = try FocusEvent.initTrusted(comptime .wrap("blur"), .{ .composed = true }, page);
|
||||
defer if (!blur_event.asEvent()._v8_handoff) blur_event.deinit(false);
|
||||
try page._event_manager.dispatch(old_target, blur_event.asEvent());
|
||||
|
||||
// Dispatch focusout (bubbles, composed)
|
||||
const focusout_event = try FocusEvent.initTrusted(comptime .wrap("focusout"), .{ .bubbles = true, .composed = true }, page);
|
||||
defer if (!focusout_event.asEvent()._v8_handoff) focusout_event.deinit(false);
|
||||
try page._event_manager.dispatch(old_target, focusout_event.asEvent());
|
||||
}
|
||||
|
||||
pub fn getChildren(self: *Element, page: *Page) !collections.NodeLive(.child_elements) {
|
||||
@@ -1022,6 +1063,82 @@ pub fn getClientRects(self: *Element, page: *Page) ![]DOMRect {
|
||||
return ptr[0..1];
|
||||
}
|
||||
|
||||
pub fn getScrollTop(self: *Element, page: *Page) u32 {
|
||||
const pos = page._element_scroll_positions.get(self) orelse return 0;
|
||||
return pos.y;
|
||||
}
|
||||
|
||||
pub fn setScrollTop(self: *Element, value: i32, page: *Page) !void {
|
||||
const gop = try page._element_scroll_positions.getOrPut(page.arena, self);
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{};
|
||||
}
|
||||
gop.value_ptr.y = @intCast(@max(0, value));
|
||||
}
|
||||
|
||||
pub fn getScrollLeft(self: *Element, page: *Page) u32 {
|
||||
const pos = page._element_scroll_positions.get(self) orelse return 0;
|
||||
return pos.x;
|
||||
}
|
||||
|
||||
pub fn setScrollLeft(self: *Element, value: i32, page: *Page) !void {
|
||||
const gop = try page._element_scroll_positions.getOrPut(page.arena, self);
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{};
|
||||
}
|
||||
gop.value_ptr.x = @intCast(@max(0, value));
|
||||
}
|
||||
|
||||
pub fn getScrollHeight(self: *Element, page: *Page) !f64 {
|
||||
// In our dummy layout engine, content doesn't overflow
|
||||
return self.getClientHeight(page);
|
||||
}
|
||||
|
||||
pub fn getScrollWidth(self: *Element, page: *Page) !f64 {
|
||||
// In our dummy layout engine, content doesn't overflow
|
||||
return self.getClientWidth(page);
|
||||
}
|
||||
|
||||
pub fn getOffsetHeight(self: *Element, page: *Page) !f64 {
|
||||
if (!try self.checkVisibility(page)) {
|
||||
return 0.0;
|
||||
}
|
||||
const dims = try self.getElementDimensions(page);
|
||||
return dims.height;
|
||||
}
|
||||
|
||||
pub fn getOffsetWidth(self: *Element, page: *Page) !f64 {
|
||||
if (!try self.checkVisibility(page)) {
|
||||
return 0.0;
|
||||
}
|
||||
const dims = try self.getElementDimensions(page);
|
||||
return dims.width;
|
||||
}
|
||||
|
||||
pub fn getOffsetTop(self: *Element, page: *Page) !f64 {
|
||||
if (!try self.checkVisibility(page)) {
|
||||
return 0.0;
|
||||
}
|
||||
return calculateDocumentPosition(self.asNode());
|
||||
}
|
||||
|
||||
pub fn getOffsetLeft(self: *Element, page: *Page) !f64 {
|
||||
if (!try self.checkVisibility(page)) {
|
||||
return 0.0;
|
||||
}
|
||||
return calculateSiblingPosition(self.asNode());
|
||||
}
|
||||
|
||||
pub fn getClientTop(_: *Element) f64 {
|
||||
// Border width - in our dummy layout, we don't apply borders to layout
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
pub fn getClientLeft(_: *Element) f64 {
|
||||
// Border width - in our dummy layout, we don't apply borders to layout
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Calculates document position by counting all nodes that appear before this one
|
||||
// in tree order, but only traversing the "left side" of the tree.
|
||||
//
|
||||
@@ -1105,47 +1222,16 @@ fn calculateSiblingPosition(node: *Node) f64 {
|
||||
return position * 5.0; // 5px per node
|
||||
}
|
||||
|
||||
const GetElementsByTagNameResult = union(enum) {
|
||||
tag: collections.NodeLive(.tag),
|
||||
tag_name: collections.NodeLive(.tag_name),
|
||||
all_elements: collections.NodeLive(.all_elements),
|
||||
};
|
||||
pub fn getElementsByTagName(self: *Element, tag_name: []const u8, page: *Page) !GetElementsByTagNameResult {
|
||||
if (tag_name.len > 256) {
|
||||
// 256 seems generous.
|
||||
return error.InvalidTagName;
|
||||
}
|
||||
pub fn getElementsByTagName(self: *Element, tag_name: []const u8, page: *Page) !Node.GetElementsByTagNameResult {
|
||||
return self.asNode().getElementsByTagName(tag_name, page);
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, tag_name, "*")) {
|
||||
return .{
|
||||
.all_elements = collections.NodeLive(.all_elements).init(self.asNode(), {}, page),
|
||||
};
|
||||
}
|
||||
|
||||
const lower = std.ascii.lowerString(&page.buf, tag_name);
|
||||
if (Tag.parseForMatch(lower)) |known| {
|
||||
// optimized for known tag names
|
||||
return .{
|
||||
.tag = collections.NodeLive(.tag).init(self.asNode(), known, page),
|
||||
};
|
||||
}
|
||||
|
||||
const arena = page.arena;
|
||||
const filter = try String.init(arena, lower, .{});
|
||||
return .{ .tag_name = collections.NodeLive(.tag_name).init(self.asNode(), filter, page) };
|
||||
pub fn getElementsByTagNameNS(self: *Element, namespace: ?[]const u8, local_name: []const u8, page: *Page) !collections.NodeLive(.tag_name_ns) {
|
||||
return self.asNode().getElementsByTagNameNS(namespace, local_name, page);
|
||||
}
|
||||
|
||||
pub fn getElementsByClassName(self: *Element, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) {
|
||||
const arena = page.arena;
|
||||
|
||||
// Parse space-separated class names
|
||||
var class_names: std.ArrayList([]const u8) = .empty;
|
||||
var it = std.mem.tokenizeAny(u8, class_name, "\t\n\x0C\r ");
|
||||
while (it.next()) |name| {
|
||||
try class_names.append(arena, try page.dupeString(name));
|
||||
}
|
||||
|
||||
return collections.NodeLive(.class_name).init(self.asNode(), class_names.items, page);
|
||||
return self.asNode().getElementsByClassName(class_name, page);
|
||||
}
|
||||
|
||||
pub fn clone(self: *Element, deep: bool, page: *Page) !*Node {
|
||||
@@ -1219,6 +1305,7 @@ pub fn getTag(self: *const Element) Tag {
|
||||
.area => .area,
|
||||
.base => .base,
|
||||
.div => .div,
|
||||
.dl => .dl,
|
||||
.embed => .embed,
|
||||
.form => .form,
|
||||
.p => .p,
|
||||
@@ -1425,6 +1512,7 @@ pub const JsApi = struct {
|
||||
pub const name = "Element";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const tagName = bridge.accessor(_tagName, null, .{});
|
||||
@@ -1479,7 +1567,7 @@ pub const JsApi = struct {
|
||||
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{});
|
||||
pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{});
|
||||
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});
|
||||
pub const classList = bridge.accessor(Element.getClassList, null, .{});
|
||||
pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{});
|
||||
pub const dataset = bridge.accessor(Element.getDataset, null, .{});
|
||||
pub const style = bridge.accessor(Element.getStyle, null, .{});
|
||||
pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{});
|
||||
@@ -1527,9 +1615,20 @@ pub const JsApi = struct {
|
||||
pub const checkVisibility = bridge.function(Element.checkVisibility, .{});
|
||||
pub const clientWidth = bridge.accessor(Element.getClientWidth, null, .{});
|
||||
pub const clientHeight = bridge.accessor(Element.getClientHeight, null, .{});
|
||||
pub const clientTop = bridge.accessor(Element.getClientTop, null, .{});
|
||||
pub const clientLeft = bridge.accessor(Element.getClientLeft, null, .{});
|
||||
pub const scrollTop = bridge.accessor(Element.getScrollTop, Element.setScrollTop, .{});
|
||||
pub const scrollLeft = bridge.accessor(Element.getScrollLeft, Element.setScrollLeft, .{});
|
||||
pub const scrollHeight = bridge.accessor(Element.getScrollHeight, null, .{});
|
||||
pub const scrollWidth = bridge.accessor(Element.getScrollWidth, null, .{});
|
||||
pub const offsetTop = bridge.accessor(Element.getOffsetTop, null, .{});
|
||||
pub const offsetLeft = bridge.accessor(Element.getOffsetLeft, null, .{});
|
||||
pub const offsetWidth = bridge.accessor(Element.getOffsetWidth, null, .{});
|
||||
pub const offsetHeight = bridge.accessor(Element.getOffsetHeight, null, .{});
|
||||
pub const getClientRects = bridge.function(Element.getClientRects, .{});
|
||||
pub const getBoundingClientRect = bridge.function(Element.getBoundingClientRect, .{});
|
||||
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
|
||||
pub const getElementsByTagNameNS = bridge.function(Element.getElementsByTagNameNS, .{});
|
||||
pub const getElementsByClassName = bridge.function(Element.getElementsByClassName, .{});
|
||||
pub const children = bridge.accessor(Element.getChildren, null, .{});
|
||||
pub const focus = bridge.function(Element.focus, .{});
|
||||
|
||||
@@ -409,6 +409,7 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const weak = true;
|
||||
pub const finalizer = bridge.finalizer(Event.deinit);
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(Event.init, .{});
|
||||
|
||||
@@ -39,7 +39,7 @@ pub const Type = union(enum) {
|
||||
media_query_list: *@import("css/MediaQueryList.zig"),
|
||||
message_port: *@import("MessagePort.zig"),
|
||||
text_track_cue: *@import("media/TextTrackCue.zig"),
|
||||
navigation: *@import("navigation/NavigationEventTarget.zig"),
|
||||
navigation: *@import("navigation/Navigation.zig"),
|
||||
screen: *@import("Screen.zig"),
|
||||
screen_orientation: *@import("Screen.zig").Orientation,
|
||||
visual_viewport: *@import("VisualViewport.zig"),
|
||||
@@ -162,6 +162,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(EventTarget.init, .{});
|
||||
|
||||
122
src/browser/webapi/ImageData.zig
Normal file
122
src/browser/webapi/ImageData.zig
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (C) 2023-2026 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 String = @import("../../string.zig").String;
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const color = @import("../color.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/ImageData/ImageData
|
||||
const ImageData = @This();
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
_data: js.ArrayBufferRef(.uint8_clamped).Global,
|
||||
|
||||
pub const ConstructorSettings = struct {
|
||||
/// Specifies the color space of the image data.
|
||||
/// Can be set to "srgb" for the sRGB color space or "display-p3" for the display-p3 color space.
|
||||
colorSpace: String = .wrap("srgb"),
|
||||
/// Specifies the pixel format.
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createImageData#pixelformat
|
||||
pixelFormat: String = .wrap("rgba-unorm8"),
|
||||
};
|
||||
|
||||
/// This has many constructors:
|
||||
///
|
||||
/// ```js
|
||||
/// new ImageData(width, height)
|
||||
/// new ImageData(width, height, settings)
|
||||
///
|
||||
/// new ImageData(dataArray, width)
|
||||
/// new ImageData(dataArray, width, height)
|
||||
/// new ImageData(dataArray, width, height, settings)
|
||||
/// ```
|
||||
///
|
||||
/// We currently support only the first 2.
|
||||
pub fn constructor(
|
||||
width: u32,
|
||||
height: u32,
|
||||
maybe_settings: ?ConstructorSettings,
|
||||
page: *Page,
|
||||
) !*ImageData {
|
||||
if (width == 0 or height == 0) {
|
||||
return error.IndexSizeError;
|
||||
}
|
||||
|
||||
const settings: ConstructorSettings = maybe_settings orelse .{};
|
||||
if (settings.colorSpace.eql(comptime .wrap("srgb")) == false) {
|
||||
return error.TypeError;
|
||||
}
|
||||
if (settings.pixelFormat.eql(comptime .wrap("rgba-unorm8")) == false) {
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
const size = width * height * 4;
|
||||
return page._factory.create(ImageData{
|
||||
._width = width,
|
||||
._height = height,
|
||||
._data = try page.js.local.?.createTypedArray(.uint8_clamped, size).persist(),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn getWidth(self: *const ImageData) u32 {
|
||||
return self._width;
|
||||
}
|
||||
|
||||
pub fn getHeight(self: *const ImageData) u32 {
|
||||
return self._height;
|
||||
}
|
||||
|
||||
pub fn getPixelFormat(_: *const ImageData) String {
|
||||
return comptime .wrap("rgba-unorm8");
|
||||
}
|
||||
|
||||
pub fn getColorSpace(_: *const ImageData) String {
|
||||
return comptime .wrap("srgb");
|
||||
}
|
||||
|
||||
pub fn getData(self: *const ImageData) js.ArrayBufferRef(.uint8_clamped).Global {
|
||||
return self._data;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(ImageData);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "ImageData";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(ImageData.constructor, .{ .dom_exception = true });
|
||||
|
||||
pub const width = bridge.accessor(ImageData.getWidth, null, .{});
|
||||
pub const height = bridge.accessor(ImageData.getHeight, null, .{});
|
||||
pub const pixelFormat = bridge.accessor(ImageData.getPixelFormat, null, .{});
|
||||
pub const colorSpace = bridge.accessor(ImageData.getColorSpace, null, .{});
|
||||
pub const data = bridge.accessor(ImageData.getData, null, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "WebApi: ImageData" {
|
||||
try testing.htmlRunner("image_data.html", .{});
|
||||
}
|
||||
@@ -246,7 +246,7 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
|
||||
._page = page,
|
||||
._arena = arena,
|
||||
._target = target,
|
||||
._time = 0.0, // TODO: Get actual timestamp
|
||||
._time = page.window._performance.now(),
|
||||
._bounding_client_rect = data.bounding_client_rect,
|
||||
._intersection_rect = data.intersection_rect,
|
||||
._root_bounds = data.root_bounds,
|
||||
|
||||
@@ -249,13 +249,13 @@ pub fn appendChild(self: *Node, child: *Node, page: *Page) !*Node {
|
||||
return child;
|
||||
}
|
||||
|
||||
pub fn childNodes(self: *const Node, page: *Page) !*collections.ChildNodes {
|
||||
return collections.ChildNodes.init(self._children, page);
|
||||
pub fn childNodes(self: *Node, page: *Page) !*collections.ChildNodes {
|
||||
return collections.ChildNodes.init(self, page);
|
||||
}
|
||||
|
||||
pub fn getTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!void {
|
||||
switch (self._type) {
|
||||
.element => {
|
||||
.element, .document_fragment => {
|
||||
var it = self.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
// ignore comments and processing instructions.
|
||||
@@ -268,7 +268,6 @@ pub fn getTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!vo
|
||||
.cdata => |c| try writer.writeAll(c.getData()),
|
||||
.document => {},
|
||||
.document_type => {},
|
||||
.document_fragment => {},
|
||||
.attribute => |attr| try writer.writeAll(attr._value.str()),
|
||||
}
|
||||
}
|
||||
@@ -414,7 +413,9 @@ pub fn getRootNode(self: *Node, opts_: ?GetRootNodeOpts) *Node {
|
||||
return root;
|
||||
}
|
||||
|
||||
pub fn contains(self: *const Node, child: *const Node) bool {
|
||||
pub fn contains(self: *const Node, child_: ?*const Node) bool {
|
||||
const child = child_ orelse return false;
|
||||
|
||||
if (self == child) {
|
||||
// yes, this is correct
|
||||
return true;
|
||||
@@ -846,6 +847,69 @@ fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), pag
|
||||
}
|
||||
}
|
||||
|
||||
pub const GetElementsByTagNameResult = union(enum) {
|
||||
tag: collections.NodeLive(.tag),
|
||||
tag_name: collections.NodeLive(.tag_name),
|
||||
all_elements: collections.NodeLive(.all_elements),
|
||||
};
|
||||
// Not exposed in the WebAPI, but used by both Element and Document
|
||||
pub fn getElementsByTagName(self: *Node, tag_name: []const u8, page: *Page) !GetElementsByTagNameResult {
|
||||
if (tag_name.len > 256) {
|
||||
// 256 seems generous.
|
||||
return error.InvalidTagName;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, tag_name, "*")) {
|
||||
return .{
|
||||
.all_elements = collections.NodeLive(.all_elements).init(self, {}, page),
|
||||
};
|
||||
}
|
||||
|
||||
const lower = std.ascii.lowerString(&page.buf, tag_name);
|
||||
if (Node.Element.Tag.parseForMatch(lower)) |known| {
|
||||
// optimized for known tag names, comparis
|
||||
return .{
|
||||
.tag = collections.NodeLive(.tag).init(self, known, page),
|
||||
};
|
||||
}
|
||||
|
||||
const arena = page.arena;
|
||||
const filter = try String.init(arena, lower, .{});
|
||||
return .{ .tag_name = collections.NodeLive(.tag_name).init(self, filter, page) };
|
||||
}
|
||||
|
||||
// Not exposed in the WebAPI, but used by both Element and Document
|
||||
pub fn getElementsByTagNameNS(self: *Node, namespace: ?[]const u8, local_name: []const u8, page: *Page) !collections.NodeLive(.tag_name_ns) {
|
||||
if (local_name.len > 256) {
|
||||
return error.InvalidTagName;
|
||||
}
|
||||
|
||||
// Parse namespace - "*" means wildcard (null), null means Element.Namespace.null
|
||||
const ns: ?Element.Namespace = if (namespace) |ns_str|
|
||||
if (std.mem.eql(u8, ns_str, "*")) null else Element.Namespace.parse(ns_str)
|
||||
else
|
||||
Element.Namespace.null;
|
||||
|
||||
return collections.NodeLive(.tag_name_ns).init(self, .{
|
||||
.namespace = ns,
|
||||
.local_name = try String.init(page.arena, local_name, .{}),
|
||||
}, page);
|
||||
}
|
||||
|
||||
// Not exposed in the WebAPI, but used by both Element and Document
|
||||
pub fn getElementsByClassName(self: *Node, class_name: []const u8, page: *Page) !collections.NodeLive(.class_name) {
|
||||
const arena = page.arena;
|
||||
|
||||
// Parse space-separated class names
|
||||
var class_names: std.ArrayList([]const u8) = .empty;
|
||||
var it = std.mem.tokenizeAny(u8, class_name, "\t\n\x0C\r ");
|
||||
while (it.next()) |name| {
|
||||
try class_names.append(arena, try page.dupeString(name));
|
||||
}
|
||||
|
||||
return collections.NodeLive(.class_name).init(self, class_names.items, page);
|
||||
}
|
||||
|
||||
// Writes a JSON representation of the node and its children
|
||||
pub fn jsonStringify(self: *const Node, writer: *std.json.Stringify) !void {
|
||||
// stupid json api requires this to be const,
|
||||
@@ -879,6 +943,7 @@ pub const JsApi = struct {
|
||||
pub const name = "Node";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const ELEMENT_NODE = bridge.property(1, .{ .template = true });
|
||||
@@ -912,16 +977,15 @@ pub const JsApi = struct {
|
||||
fn _textContext(self: *Node, page: *const Page) !?[]const u8 {
|
||||
// cdata and attributes can return value directly, avoiding the copy
|
||||
switch (self._type) {
|
||||
.element => |el| {
|
||||
.element, .document_fragment => {
|
||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
try el.asNode().getTextContent(&buf.writer);
|
||||
try self.getTextContent(&buf.writer);
|
||||
return buf.written();
|
||||
},
|
||||
.cdata => |cdata| return cdata.getData(),
|
||||
.attribute => |attr| return attr._value.str(),
|
||||
.document => return null,
|
||||
.document_type => return null,
|
||||
.document_fragment => return null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,7 +996,7 @@ pub const JsApi = struct {
|
||||
pub const parentNode = bridge.accessor(Node.parentNode, null, .{});
|
||||
pub const parentElement = bridge.accessor(Node.parentElement, null, .{});
|
||||
pub const appendChild = bridge.function(Node.appendChild, .{ .dom_exception = true });
|
||||
pub const childNodes = bridge.accessor(Node.childNodes, null, .{});
|
||||
pub const childNodes = bridge.accessor(Node.childNodes, null, .{ .cache = .{ .private = "child_nodes" } });
|
||||
pub const isConnected = bridge.accessor(Node.isConnected, null, .{});
|
||||
pub const ownerDocument = bridge.accessor(Node.ownerDocument, null, .{});
|
||||
pub const hasChildNodes = bridge.function(Node.hasChildNodes, .{});
|
||||
|
||||
@@ -88,6 +88,7 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const empty_with_no_proto = true;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const FILTER_ACCEPT = bridge.property(NodeFilter.FILTER_ACCEPT, .{ .template = true });
|
||||
|
||||
@@ -113,6 +113,20 @@ pub fn observe(
|
||||
|
||||
// Update interests.
|
||||
self._interests = interests;
|
||||
|
||||
// Deliver existing entries if buffered option is set.
|
||||
// Per spec, buffered is only valid with the type option, not entryTypes.
|
||||
// Delivery is async via a queued task, not synchronous.
|
||||
if (options.buffered and options.type != null and !self.hasRecords()) {
|
||||
for (page.window._performance._entries.items) |entry| {
|
||||
if (self.interested(entry)) {
|
||||
try self._entries.append(page.arena, entry);
|
||||
}
|
||||
}
|
||||
if (self.hasRecords()) {
|
||||
try page.schedulePerformanceObserverDelivery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnect(self: *PerformanceObserver, page: *Page) void {
|
||||
|
||||
@@ -37,6 +37,10 @@ pub fn init(page: *Page) !*Range {
|
||||
}
|
||||
|
||||
pub fn setStart(self: *Range, node: *Node, offset: u32) !void {
|
||||
if (node._type == .document_type) {
|
||||
return error.InvalidNodeType;
|
||||
}
|
||||
|
||||
if (offset > node.getLength()) {
|
||||
return error.IndexSizeError;
|
||||
}
|
||||
@@ -54,6 +58,10 @@ pub fn setStart(self: *Range, node: *Node, offset: u32) !void {
|
||||
}
|
||||
|
||||
pub fn setEnd(self: *Range, node: *Node, offset: u32) !void {
|
||||
if (node._type == .document_type) {
|
||||
return error.InvalidNodeType;
|
||||
}
|
||||
|
||||
// Validate offset
|
||||
if (offset > node.getLength()) {
|
||||
return error.IndexSizeError;
|
||||
@@ -150,10 +158,10 @@ pub fn compareBoundaryPoints(self: *const Range, how_raw: i32, source_range: *co
|
||||
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,
|
||||
self._proto._end_container,
|
||||
self._proto._end_offset,
|
||||
source_range._proto._start_container,
|
||||
source_range._proto._start_offset,
|
||||
),
|
||||
2 => AbstractRange.compareBoundaryPoints( // END_TO_END
|
||||
self._proto._end_container,
|
||||
@@ -162,10 +170,10 @@ pub fn compareBoundaryPoints(self: *const Range, how_raw: i32, source_range: *co
|
||||
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,
|
||||
self._proto._start_container,
|
||||
self._proto._start_offset,
|
||||
source_range._proto._end_container,
|
||||
source_range._proto._end_offset,
|
||||
),
|
||||
else => unreachable,
|
||||
};
|
||||
@@ -178,10 +186,6 @@ pub fn compareBoundaryPoints(self: *const Range, how_raw: i32, source_range: *co
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -189,6 +193,14 @@ pub fn comparePoint(self: *const Range, node: *Node, offset: u32) !i16 {
|
||||
return error.WrongDocument;
|
||||
}
|
||||
|
||||
if (node._type == .document_type) {
|
||||
return error.InvalidNodeType;
|
||||
}
|
||||
|
||||
if (offset > node.getLength()) {
|
||||
return error.IndexSizeError;
|
||||
}
|
||||
|
||||
// Compare point with start boundary
|
||||
const cmp_start = AbstractRange.compareBoundaryPoints(
|
||||
node,
|
||||
@@ -346,6 +358,7 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
|
||||
if (self._proto.getCollapsed()) {
|
||||
return;
|
||||
}
|
||||
page.domChanged();
|
||||
|
||||
// Simple case: same container
|
||||
if (self._proto._start_container == self._proto._end_container) {
|
||||
@@ -536,23 +549,93 @@ pub fn toString(self: *const Range, page: *Page) ![]const u8 {
|
||||
}
|
||||
|
||||
fn writeTextContent(self: *const Range, writer: *std.Io.Writer) !void {
|
||||
if (self._proto.getCollapsed()) {
|
||||
return;
|
||||
if (self._proto.getCollapsed()) return;
|
||||
|
||||
const start_node = self._proto._start_container;
|
||||
const end_node = self._proto._end_container;
|
||||
const start_offset = self._proto._start_offset;
|
||||
const end_offset = self._proto._end_offset;
|
||||
|
||||
// Same text node — just substring
|
||||
if (start_node == end_node) {
|
||||
if (start_node.is(Node.CData)) |cdata| {
|
||||
if (!isCommentOrPI(cdata)) {
|
||||
const data = cdata.getData();
|
||||
const s = @min(start_offset, data.len);
|
||||
const e = @min(end_offset, data.len);
|
||||
try writer.writeAll(data[s..e]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (self._proto._start_container == self._proto._end_container) {
|
||||
if (self._proto._start_container.is(Node.CData)) |cdata| {
|
||||
const root = self._proto.getCommonAncestorContainer();
|
||||
|
||||
// Partial start: if start container is a text node, write from offset to end
|
||||
if (start_node.is(Node.CData)) |cdata| {
|
||||
if (!isCommentOrPI(cdata)) {
|
||||
const data = cdata.getData();
|
||||
if (self._proto._start_offset < data.len and self._proto._end_offset <= data.len) {
|
||||
try writer.writeAll(data[self._proto._start_offset..self._proto._end_offset]);
|
||||
const s = @min(start_offset, data.len);
|
||||
try writer.writeAll(data[s..]);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk fully-contained text nodes between the boundaries.
|
||||
// For text containers, the walk starts after that node.
|
||||
// For element containers, the walk starts at the child at offset.
|
||||
const walk_start: ?*Node = if (start_node.is(Node.CData) != null)
|
||||
nextInTreeOrder(start_node, root)
|
||||
else
|
||||
start_node.getChildAt(start_offset) orelse nextAfterSubtree(start_node, root);
|
||||
|
||||
const walk_end: ?*Node = if (end_node.is(Node.CData) != null)
|
||||
end_node
|
||||
else
|
||||
end_node.getChildAt(end_offset) orelse nextAfterSubtree(end_node, root);
|
||||
|
||||
if (walk_start) |start| {
|
||||
var current: ?*Node = start;
|
||||
while (current) |n| {
|
||||
if (walk_end) |we| {
|
||||
if (n == we) break;
|
||||
}
|
||||
if (n.is(Node.CData)) |cdata| {
|
||||
if (!isCommentOrPI(cdata)) {
|
||||
try writer.writeAll(cdata.getData());
|
||||
}
|
||||
}
|
||||
current = nextInTreeOrder(n, root);
|
||||
}
|
||||
}
|
||||
|
||||
// Partial end: if end container is a different text node, write from start to offset
|
||||
if (start_node != end_node) {
|
||||
if (end_node.is(Node.CData)) |cdata| {
|
||||
if (!isCommentOrPI(cdata)) {
|
||||
const data = cdata.getData();
|
||||
const e = @min(end_offset, data.len);
|
||||
try writer.writeAll(data[0..e]);
|
||||
}
|
||||
}
|
||||
// For elements, would need to iterate children
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Complex case: different containers - would need proper tree walking
|
||||
// For now, just return empty
|
||||
fn isCommentOrPI(cdata: *Node.CData) bool {
|
||||
return cdata.is(Node.CData.Comment) != null or cdata.is(Node.CData.ProcessingInstruction) != null;
|
||||
}
|
||||
|
||||
fn nextInTreeOrder(node: *Node, root: *Node) ?*Node {
|
||||
if (node.firstChild()) |child| return child;
|
||||
return nextAfterSubtree(node, root);
|
||||
}
|
||||
|
||||
fn nextAfterSubtree(node: *Node, root: *Node) ?*Node {
|
||||
var current = node;
|
||||
while (current != root) {
|
||||
if (current.nextSibling()) |sibling| return sibling;
|
||||
current = current.parentNode() orelse return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -32,13 +32,6 @@ const Screen = @This();
|
||||
_proto: *EventTarget,
|
||||
_orientation: ?*Orientation = null,
|
||||
|
||||
pub fn init(page: *Page) !*Screen {
|
||||
return page._factory.eventTarget(Screen{
|
||||
._proto = undefined,
|
||||
._orientation = null,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn asEventTarget(self: *Screen) *EventTarget {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ const lp = @import("lightpanda");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const crypto = @import("../../crypto.zig");
|
||||
const DOMException = @import("DOMException.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const js = @import("../js/js.zig");
|
||||
@@ -218,6 +219,35 @@ pub fn verify(
|
||||
};
|
||||
}
|
||||
|
||||
pub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise {
|
||||
const local = page.js.local.?;
|
||||
if (algorithm.len > 10) {
|
||||
return local.rejectPromise(DOMException.fromError(error.NotSupported));
|
||||
}
|
||||
const normalized = std.ascii.lowerString(&page.buf, algorithm);
|
||||
if (std.mem.eql(u8, normalized, "sha-1")) {
|
||||
const Sha1 = std.crypto.hash.Sha1;
|
||||
Sha1.hash(data.values, page.buf[0..Sha1.digest_length], .{});
|
||||
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha1.digest_length] });
|
||||
}
|
||||
if (std.mem.eql(u8, normalized, "sha-256")) {
|
||||
const Sha256 = std.crypto.hash.sha2.Sha256;
|
||||
Sha256.hash(data.values, page.buf[0..Sha256.digest_length], .{});
|
||||
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha256.digest_length] });
|
||||
}
|
||||
if (std.mem.eql(u8, normalized, "sha-384")) {
|
||||
const Sha384 = std.crypto.hash.sha2.Sha384;
|
||||
Sha384.hash(data.values, page.buf[0..Sha384.digest_length], .{});
|
||||
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha384.digest_length] });
|
||||
}
|
||||
if (std.mem.eql(u8, normalized, "sha-512")) {
|
||||
const Sha512 = std.crypto.hash.sha2.Sha512;
|
||||
Sha512.hash(data.values, page.buf[0..Sha512.digest_length], .{});
|
||||
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha512.digest_length] });
|
||||
}
|
||||
return local.rejectPromise(DOMException.fromError(error.NotSupported));
|
||||
}
|
||||
|
||||
/// Returns the desired digest by its name.
|
||||
fn findDigest(name: []const u8) error{Invalid}!*const crypto.EVP_MD {
|
||||
if (std.mem.eql(u8, "SHA-256", name)) {
|
||||
@@ -354,7 +384,7 @@ pub const CryptoKey = struct {
|
||||
.object => |obj| obj.name,
|
||||
};
|
||||
// Find digest.
|
||||
const digest = try findDigest(hash);
|
||||
const d = try findDigest(hash);
|
||||
|
||||
// We need at least a single usage.
|
||||
if (key_usages.len == 0) {
|
||||
@@ -380,7 +410,7 @@ pub const CryptoKey = struct {
|
||||
break :blk length / 8;
|
||||
}
|
||||
// Prefer block size of the hash function instead.
|
||||
break :blk crypto.EVP_MD_block_size(digest);
|
||||
break :blk crypto.EVP_MD_block_size(d);
|
||||
};
|
||||
|
||||
const key = try page.arena.alloc(u8, block_size);
|
||||
@@ -395,7 +425,7 @@ pub const CryptoKey = struct {
|
||||
._extractable = extractable,
|
||||
._usages = usages_mask,
|
||||
._key = key,
|
||||
._vary = .{ .digest = digest },
|
||||
._vary = .{ .digest = d },
|
||||
});
|
||||
|
||||
return .{ .key = crypto_key };
|
||||
@@ -635,4 +665,5 @@ pub const JsApi = struct {
|
||||
pub const sign = bridge.function(SubtleCrypto.sign, .{ .dom_exception = true });
|
||||
pub const verify = bridge.function(SubtleCrypto.verify, .{ .dom_exception = true });
|
||||
pub const deriveBits = bridge.function(SubtleCrypto.deriveBits, .{ .dom_exception = true });
|
||||
pub const digest = bridge.function(SubtleCrypto.digest, .{ .dom_exception = true });
|
||||
};
|
||||
|
||||
@@ -25,12 +25,6 @@ const VisualViewport = @This();
|
||||
|
||||
_proto: *EventTarget,
|
||||
|
||||
pub fn init(page: *Page) !*VisualViewport {
|
||||
return page._factory.eventTarget(VisualViewport{
|
||||
._proto = undefined,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn asEventTarget(self: *VisualViewport) *EventTarget {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ _navigator: Navigator = .init,
|
||||
_screen: *Screen,
|
||||
_visual_viewport: *VisualViewport,
|
||||
_performance: Performance,
|
||||
_storage_bucket: *storage.Bucket,
|
||||
_storage_bucket: storage.Bucket = .{},
|
||||
_on_load: ?js.Function.Global = null,
|
||||
_on_pageshow: ?js.Function.Global = null,
|
||||
_on_popstate: ?js.Function.Global = null,
|
||||
@@ -128,11 +128,11 @@ pub fn getPerformance(self: *Window) *Performance {
|
||||
return &self._performance;
|
||||
}
|
||||
|
||||
pub fn getLocalStorage(self: *const Window) *storage.Lookup {
|
||||
pub fn getLocalStorage(self: *Window) *storage.Lookup {
|
||||
return &self._storage_bucket.local;
|
||||
}
|
||||
|
||||
pub fn getSessionStorage(self: *const Window) *storage.Lookup {
|
||||
pub fn getSessionStorage(self: *Window) *storage.Lookup {
|
||||
return &self._storage_bucket.session;
|
||||
}
|
||||
|
||||
@@ -713,18 +713,19 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = .{ .internal = 1 } });
|
||||
pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = .{ .internal = 2 } });
|
||||
|
||||
pub const top = bridge.accessor(Window.getWindow, null, .{});
|
||||
pub const self = bridge.accessor(Window.getWindow, null, .{});
|
||||
pub const window = bridge.accessor(Window.getWindow, null, .{});
|
||||
pub const parent = bridge.accessor(Window.getWindow, null, .{});
|
||||
pub const console = bridge.accessor(Window.getConsole, null, .{});
|
||||
pub const navigator = bridge.accessor(Window.getNavigator, null, .{});
|
||||
pub const screen = bridge.accessor(Window.getScreen, null, .{});
|
||||
pub const visualViewport = bridge.accessor(Window.getVisualViewport, null, .{});
|
||||
pub const performance = bridge.accessor(Window.getPerformance, null, .{});
|
||||
pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{});
|
||||
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{});
|
||||
pub const document = bridge.accessor(Window.getDocument, null, .{});
|
||||
pub const location = bridge.accessor(Window.getLocation, Window.setLocation, .{});
|
||||
pub const history = bridge.accessor(Window.getHistory, null, .{});
|
||||
pub const navigation = bridge.accessor(Window.getNavigation, null, .{});
|
||||
@@ -774,9 +775,25 @@ pub const JsApi = struct {
|
||||
|
||||
pub const innerWidth = bridge.property(1920, .{ .template = false });
|
||||
pub const innerHeight = bridge.property(1080, .{ .template = false });
|
||||
pub const devicePixelRatio = bridge.property(1, .{ .template = false });
|
||||
|
||||
// This should return a window-like object in specific conditions. Would be
|
||||
// pretty complicated to properly support I think.
|
||||
pub const opener = bridge.property(null, .{ .template = false });
|
||||
|
||||
pub const alert = bridge.function(struct {
|
||||
fn alert(_: *const Window, _: ?[]const u8) void {}
|
||||
}.alert, .{});
|
||||
pub const confirm = bridge.function(struct {
|
||||
fn confirm(_: *const Window, _: ?[]const u8) bool {
|
||||
return false;
|
||||
}
|
||||
}.confirm, .{});
|
||||
pub const prompt = bridge.function(struct {
|
||||
fn prompt(_: *const Window, _: ?[]const u8, _: ?[]const u8) ?[]const u8 {
|
||||
return null;
|
||||
}
|
||||
}.prompt, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -37,6 +37,7 @@ pub const JsApi = struct {
|
||||
pub const name = "Comment";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(Comment.init, .{});
|
||||
|
||||
@@ -36,6 +36,7 @@ pub const JsApi = struct {
|
||||
pub const name = "ProcessingInstruction";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const target = bridge.accessor(ProcessingInstruction.getTarget, null, .{});
|
||||
|
||||
@@ -69,6 +69,7 @@ pub const JsApi = struct {
|
||||
pub const name = "Text";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(Text.init, .{});
|
||||
|
||||
@@ -30,18 +30,18 @@ _last_index: usize,
|
||||
_last_length: ?u32,
|
||||
_last_node: ?*std.DoublyLinkedList.Node,
|
||||
_cached_version: usize,
|
||||
_children: ?*Node.Children,
|
||||
_node: *Node,
|
||||
|
||||
pub const KeyIterator = GenericIterator(Iterator, "0");
|
||||
pub const ValueIterator = GenericIterator(Iterator, "1");
|
||||
pub const EntryIterator = GenericIterator(Iterator, null);
|
||||
|
||||
pub fn init(children: ?*Node.Children, page: *Page) !*ChildNodes {
|
||||
pub fn init(node: *Node, page: *Page) !*ChildNodes {
|
||||
return page._factory.create(ChildNodes{
|
||||
._node = node,
|
||||
._last_index = 0,
|
||||
._last_node = null,
|
||||
._last_length = null,
|
||||
._children = children,
|
||||
._cached_version = page.version,
|
||||
});
|
||||
}
|
||||
@@ -52,7 +52,7 @@ pub fn length(self: *ChildNodes, page: *Page) !u32 {
|
||||
return cached_length;
|
||||
}
|
||||
}
|
||||
const children = self._children orelse return 0;
|
||||
const children = self._node._children orelse return 0;
|
||||
|
||||
// O(N)
|
||||
const len = children.len();
|
||||
@@ -65,13 +65,13 @@ pub fn getAtIndex(self: *ChildNodes, index: usize, page: *Page) !?*Node {
|
||||
|
||||
var current = self._last_index;
|
||||
var node: ?*std.DoublyLinkedList.Node = null;
|
||||
if (index <= current) {
|
||||
if (index < current) {
|
||||
current = 0;
|
||||
node = self.first() orelse return null;
|
||||
} else {
|
||||
node = self._last_node orelse self.first() orelse return null;
|
||||
}
|
||||
defer self._last_index = current + 1;
|
||||
defer self._last_index = current;
|
||||
|
||||
while (node) |n| {
|
||||
if (index == current) {
|
||||
@@ -86,7 +86,7 @@ pub fn getAtIndex(self: *ChildNodes, index: usize, page: *Page) !?*Node {
|
||||
}
|
||||
|
||||
pub fn first(self: *const ChildNodes) ?*std.DoublyLinkedList.Node {
|
||||
return &(self._children orelse return null).first()._child_link;
|
||||
return &(self._node._children orelse return null).first()._child_link;
|
||||
}
|
||||
|
||||
pub fn keys(self: *ChildNodes, page: *Page) !*KeyIterator {
|
||||
|
||||
@@ -138,21 +138,59 @@ pub fn toggle(self: *DOMTokenList, token: []const u8, force: ?bool, page: *Page)
|
||||
}
|
||||
|
||||
pub fn replace(self: *DOMTokenList, old_token: []const u8, new_token: []const u8, page: *Page) !bool {
|
||||
try validateToken(old_token);
|
||||
try validateToken(new_token);
|
||||
// Validate in spec order: both empty first, then both whitespace
|
||||
if (old_token.len == 0 or new_token.len == 0) {
|
||||
return error.SyntaxError;
|
||||
}
|
||||
if (std.mem.indexOfAny(u8, old_token, WHITESPACE) != null) {
|
||||
return error.InvalidCharacterError;
|
||||
}
|
||||
if (std.mem.indexOfAny(u8, new_token, WHITESPACE) != null) {
|
||||
return error.InvalidCharacterError;
|
||||
}
|
||||
|
||||
var lookup = try self.getTokens(page);
|
||||
if (lookup.contains(new_token)) {
|
||||
if (std.mem.eql(u8, new_token, old_token) == false) {
|
||||
_ = lookup.orderedRemove(old_token);
|
||||
try self.updateAttribute(lookup, page);
|
||||
}
|
||||
|
||||
// Check if old_token exists
|
||||
if (!lookup.contains(old_token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If replacing with the same token, still need to trigger mutation
|
||||
if (std.mem.eql(u8, new_token, old_token)) {
|
||||
try self.updateAttribute(lookup, page);
|
||||
return true;
|
||||
}
|
||||
|
||||
const key_ptr = lookup.getKeyPtr(old_token) orelse return false;
|
||||
key_ptr.* = new_token;
|
||||
try self.updateAttribute(lookup, page);
|
||||
const allocator = page.call_arena;
|
||||
// Build new token list preserving order but replacing old with new
|
||||
var new_tokens = try std.ArrayList([]const u8).initCapacity(allocator, lookup.count());
|
||||
var replaced_old = false;
|
||||
|
||||
for (lookup.keys()) |token| {
|
||||
if (std.mem.eql(u8, token, old_token) and !replaced_old) {
|
||||
new_tokens.appendAssumeCapacity(new_token);
|
||||
replaced_old = true;
|
||||
} else if (std.mem.eql(u8, token, old_token)) {
|
||||
// Subsequent occurrences of old_token: skip (remove duplicates)
|
||||
continue;
|
||||
} else if (std.mem.eql(u8, token, new_token) and replaced_old) {
|
||||
// Occurrence of new_token AFTER replacement: skip (remove duplicate)
|
||||
continue;
|
||||
} else {
|
||||
// Any other token (including new_token before replacement): keep it
|
||||
new_tokens.appendAssumeCapacity(token);
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild lookup
|
||||
var new_lookup: Lookup = .empty;
|
||||
try new_lookup.ensureTotalCapacity(allocator, new_tokens.items.len);
|
||||
for (new_tokens.items) |token| {
|
||||
try new_lookup.put(allocator, token, {});
|
||||
}
|
||||
|
||||
try self.updateAttribute(new_lookup, page);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -226,8 +264,16 @@ fn validateToken(token: []const u8) !void {
|
||||
}
|
||||
|
||||
fn updateAttribute(self: *DOMTokenList, tokens: Lookup, page: *Page) !void {
|
||||
const joined = try std.mem.join(page.call_arena, " ", tokens.keys());
|
||||
try self._element.setAttribute(self._attribute_name, .wrap(joined), page);
|
||||
if (tokens.count() > 0) {
|
||||
const joined = try std.mem.join(page.call_arena, " ", tokens.keys());
|
||||
return self._element.setAttribute(self._attribute_name, .wrap(joined), page);
|
||||
}
|
||||
|
||||
// Only remove attribute if it didn't exist before (was null)
|
||||
// If it existed (even as ""), set it to "" to preserve its existence
|
||||
if (self._element.hasAttributeSafe(self._attribute_name)) {
|
||||
try self._element.setAttribute(self._attribute_name, .wrap(""), page);
|
||||
}
|
||||
}
|
||||
|
||||
const Iterator = struct {
|
||||
@@ -251,6 +297,7 @@ pub const JsApi = struct {
|
||||
pub const name = "DOMTokenList";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const length = bridge.accessor(DOMTokenList.length, null, .{});
|
||||
|
||||
@@ -27,6 +27,7 @@ const NodeLive = @import("node_live.zig").NodeLive;
|
||||
const Mode = enum {
|
||||
tag,
|
||||
tag_name,
|
||||
tag_name_ns,
|
||||
class_name,
|
||||
all_elements,
|
||||
child_elements,
|
||||
@@ -42,6 +43,7 @@ const HTMLCollection = @This();
|
||||
_data: union(Mode) {
|
||||
tag: NodeLive(.tag),
|
||||
tag_name: NodeLive(.tag_name),
|
||||
tag_name_ns: NodeLive(.tag_name_ns),
|
||||
class_name: NodeLive(.class_name),
|
||||
all_elements: NodeLive(.all_elements),
|
||||
child_elements: NodeLive(.child_elements),
|
||||
@@ -76,6 +78,7 @@ pub fn iterator(self: *HTMLCollection, page: *Page) !*Iterator {
|
||||
.tw = switch (self._data) {
|
||||
.tag => |*impl| .{ .tag = impl._tw.clone() },
|
||||
.tag_name => |*impl| .{ .tag_name = impl._tw.clone() },
|
||||
.tag_name_ns => |*impl| .{ .tag_name_ns = impl._tw.clone() },
|
||||
.class_name => |*impl| .{ .class_name = impl._tw.clone() },
|
||||
.all_elements => |*impl| .{ .all_elements = impl._tw.clone() },
|
||||
.child_elements => |*impl| .{ .child_elements = impl._tw.clone() },
|
||||
@@ -94,6 +97,7 @@ pub const Iterator = GenericIterator(struct {
|
||||
tw: union(Mode) {
|
||||
tag: TreeWalker.FullExcludeSelf,
|
||||
tag_name: TreeWalker.FullExcludeSelf,
|
||||
tag_name_ns: TreeWalker.FullExcludeSelf,
|
||||
class_name: TreeWalker.FullExcludeSelf,
|
||||
all_elements: TreeWalker.FullExcludeSelf,
|
||||
child_elements: TreeWalker.Children,
|
||||
@@ -108,6 +112,7 @@ pub const Iterator = GenericIterator(struct {
|
||||
return switch (self.list._data) {
|
||||
.tag => |*impl| impl.nextTw(&self.tw.tag),
|
||||
.tag_name => |*impl| impl.nextTw(&self.tw.tag_name),
|
||||
.tag_name_ns => |*impl| impl.nextTw(&self.tw.tag_name_ns),
|
||||
.class_name => |*impl| impl.nextTw(&self.tw.class_name),
|
||||
.all_elements => |*impl| impl.nextTw(&self.tw.all_elements),
|
||||
.child_elements => |*impl| impl.nextTw(&self.tw.child_elements),
|
||||
@@ -127,6 +132,7 @@ pub const JsApi = struct {
|
||||
pub const name = "HTMLCollection";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const length = bridge.accessor(HTMLCollection.length, null, .{});
|
||||
|
||||
@@ -53,6 +53,10 @@ pub fn length(self: *NodeList, page: *Page) !u32 {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn indexedGet(self: *NodeList, index: usize, page: *Page) !*Node {
|
||||
return try self.getAtIndex(index, page) orelse return error.NotHandled;
|
||||
}
|
||||
|
||||
pub fn getAtIndex(self: *NodeList, index: usize, page: *Page) !?*Node {
|
||||
return switch (self.data) {
|
||||
.child_nodes => |impl| impl.getAtIndex(index, page),
|
||||
@@ -117,10 +121,11 @@ pub const JsApi = struct {
|
||||
pub const name = "NodeList";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const enumerable = false;
|
||||
};
|
||||
|
||||
pub const length = bridge.accessor(NodeList.length, null, .{});
|
||||
pub const @"[]" = bridge.indexed(NodeList.getAtIndex, .{ .null_as_undefined = true });
|
||||
pub const @"[]" = bridge.indexed(NodeList.indexedGet, .{ .null_as_undefined = true });
|
||||
pub const item = bridge.function(NodeList.getAtIndex, .{});
|
||||
pub const keys = bridge.function(NodeList.keys, .{});
|
||||
pub const values = bridge.function(NodeList.values, .{});
|
||||
|
||||
@@ -33,6 +33,7 @@ const Form = @import("../element/html/Form.zig");
|
||||
const Mode = enum {
|
||||
tag,
|
||||
tag_name,
|
||||
tag_name_ns,
|
||||
class_name,
|
||||
name,
|
||||
all_elements,
|
||||
@@ -44,9 +45,15 @@ const Mode = enum {
|
||||
form,
|
||||
};
|
||||
|
||||
pub const TagNameNsFilter = struct {
|
||||
namespace: ?Element.Namespace, // null means wildcard "*"
|
||||
local_name: String,
|
||||
};
|
||||
|
||||
const Filters = union(Mode) {
|
||||
tag: Element.Tag,
|
||||
tag_name: String,
|
||||
tag_name_ns: TagNameNsFilter,
|
||||
class_name: [][]const u8,
|
||||
name: []const u8,
|
||||
all_elements,
|
||||
@@ -83,7 +90,7 @@ const Filters = union(Mode) {
|
||||
pub fn NodeLive(comptime mode: Mode) type {
|
||||
const Filter = Filters.TypeOf(mode);
|
||||
const TW = switch (mode) {
|
||||
.tag, .tag_name, .class_name, .name, .all_elements, .links, .anchors, .form => TreeWalker.FullExcludeSelf,
|
||||
.tag, .tag_name, .tag_name_ns, .class_name, .name, .all_elements, .links, .anchors, .form => TreeWalker.FullExcludeSelf,
|
||||
.child_elements, .child_tag, .selected_options => TreeWalker.Children,
|
||||
};
|
||||
return struct {
|
||||
@@ -222,6 +229,18 @@ pub fn NodeLive(comptime mode: Mode) type {
|
||||
const element_tag = el.getTagNameLower();
|
||||
return std.mem.eql(u8, element_tag, self._filter.str());
|
||||
},
|
||||
.tag_name_ns => {
|
||||
const el = node.is(Element) orelse return false;
|
||||
if (self._filter.namespace) |ns| {
|
||||
if (el._namespace != ns) return false;
|
||||
}
|
||||
// ok, namespace matches, check local name
|
||||
if (self._filter.local_name.eql(comptime .wrap("*"))) {
|
||||
// wildcard, match-all
|
||||
return true;
|
||||
}
|
||||
return self._filter.local_name.eqlSlice(el.getLocalName());
|
||||
},
|
||||
.class_name => {
|
||||
if (self._filter.len == 0) {
|
||||
return false;
|
||||
@@ -328,6 +347,7 @@ pub fn NodeLive(comptime mode: Mode) type {
|
||||
.name => return page._factory.create(NodeList{ .data = .{ .name = self } }),
|
||||
.tag => HTMLCollection{ ._data = .{ .tag = self } },
|
||||
.tag_name => HTMLCollection{ ._data = .{ .tag_name = self } },
|
||||
.tag_name_ns => HTMLCollection{ ._data = .{ .tag_name_ns = self } },
|
||||
.class_name => HTMLCollection{ ._data = .{ .class_name = self } },
|
||||
.all_elements => HTMLCollection{ ._data = .{ .all_elements = self } },
|
||||
.child_elements => HTMLCollection{ ._data = .{ .child_elements = self } },
|
||||
|
||||
@@ -218,7 +218,7 @@ fn getDefaultDisplay(element: *const Element) []const u8 {
|
||||
.html => |html| {
|
||||
return switch (html._type) {
|
||||
.anchor, .br, .span, .label, .time, .font, .mod, .quote => "inline",
|
||||
.body, .div, .p, .heading, .form, .button, .canvas, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .legend, .map, .meter, .object, .optgroup, .output, .param, .picture, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => "block",
|
||||
.body, .div, .dl, .p, .heading, .form, .button, .canvas, .dialog, .embed, .head, .html, .hr, .iframe, .img, .input, .li, .link, .meta, .ol, .option, .script, .select, .slot, .style, .template, .textarea, .title, .ul, .media, .area, .base, .datalist, .directory, .fieldset, .legend, .map, .meter, .object, .optgroup, .output, .param, .picture, .pre, .progress, .source, .table, .table_caption, .table_cell, .table_col, .table_row, .table_section, .track => "block",
|
||||
.generic, .custom, .unknown, .data => blk: {
|
||||
const tag = element.getTagNameLower();
|
||||
if (isInlineTag(tag)) break :blk "inline";
|
||||
|
||||
@@ -37,6 +37,14 @@ pub fn asCSSStyleDeclaration(self: *CSSStyleProperties) *CSSStyleDeclaration {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
pub fn setNamed(self: *CSSStyleProperties, name: []const u8, value: []const u8, page: *Page) !void {
|
||||
if (method_names.has(name)) {
|
||||
return error.NotHandled;
|
||||
}
|
||||
const dash_case = camelCaseToDashCase(name, &page.buf);
|
||||
try self._proto.setProperty(dash_case, value, null, page);
|
||||
}
|
||||
|
||||
pub fn getNamed(self: *CSSStyleProperties, name: []const u8, page: *Page) ![]const u8 {
|
||||
if (method_names.has(name)) {
|
||||
return error.NotHandled;
|
||||
@@ -108,6 +116,9 @@ fn isKnownCSSProperty(dash_case: []const u8) bool {
|
||||
.{ "display", {} },
|
||||
.{ "visibility", {} },
|
||||
.{ "opacity", {} },
|
||||
.{ "filter", {} },
|
||||
.{ "transform", {} },
|
||||
.{ "transition", {} },
|
||||
.{ "position", {} },
|
||||
.{ "top", {} },
|
||||
.{ "bottom", {} },
|
||||
@@ -201,5 +212,5 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const @"[]" = bridge.namedIndexed(CSSStyleProperties.getNamed, null, null, .{});
|
||||
pub const @"[]" = bridge.namedIndexed(CSSStyleProperties.getNamed, CSSStyleProperties.setNamed, null, .{});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const std = @import("std");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
const Element = @import("../Element.zig");
|
||||
const CSSRuleList = @import("CSSRuleList.zig");
|
||||
const CSSRule = @import("CSSRule.zig");
|
||||
|
||||
@@ -11,14 +12,18 @@ _title: []const u8 = "",
|
||||
_disabled: bool = false,
|
||||
_css_rules: ?*CSSRuleList = null,
|
||||
_owner_rule: ?*CSSRule = null,
|
||||
_owner_node: ?*Element = null,
|
||||
|
||||
pub fn init(page: *Page) !*CSSStyleSheet {
|
||||
return page._factory.create(CSSStyleSheet{});
|
||||
}
|
||||
|
||||
pub fn getOwnerNode(self: *const CSSStyleSheet) ?*CSSStyleSheet {
|
||||
_ = self;
|
||||
return null;
|
||||
pub fn initWithOwner(owner: *Element, page: *Page) !*CSSStyleSheet {
|
||||
return page._factory.create(CSSStyleSheet{ ._owner_node = owner });
|
||||
}
|
||||
|
||||
pub fn getOwnerNode(self: *const CSSStyleSheet) ?*Element {
|
||||
return self._owner_node;
|
||||
}
|
||||
|
||||
pub fn getHref(self: *const CSSStyleSheet) ?[]const u8 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user