Execute script.onload/onerror

Add object-support for URLSearchParams. Start to treat js.Value as a first
class object (instead of js.Object, where appropriate).
This commit is contained in:
Karl Seguin
2025-11-30 12:48:15 +08:00
parent 9f587ab24b
commit 613428c54c
10 changed files with 226 additions and 84 deletions

View File

@@ -726,10 +726,10 @@ const Script = struct {
.kind = self.kind,
.cacheable = cacheable,
});
self.executeCallback(script_element._on_error, page);
self.executeCallback("error", script_element._on_error, page);
return;
};
self.executeCallback(script_element._on_load, page);
self.executeCallback("load", script_element._on_load, page);
return;
}
@@ -752,13 +752,17 @@ const Script = struct {
};
if (comptime IS_DEBUG) {
log.info(.browser, "executed script", .{.src = url});
log.debug(.browser, "executed script", .{
.src = url,
.success = success,
.on_load = script_element._on_load != null
});
}
defer page.tick();
if (success) {
self.executeCallback(script_element._on_load, page);
self.executeCallback("load", script_element._on_load, page);
return;
}
@@ -776,16 +780,31 @@ const Script = struct {
.cacheable = cacheable,
});
self.executeCallback(script_element._on_error, page);
self.executeCallback("error", script_element._on_error, page);
}
fn executeCallback(self: *const Script, cb_: ?js.Function, page: *Page) void {
fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void {
const cb = cb_ orelse return;
// @ZIGDOM execute the callback
_ = cb;
_ = self;
_ = page;
const Event = @import("webapi/Event.zig");
const event = Event.init(typ, .{}, page) catch |err| {
log.warn(.js, "script internal callback", .{
.url = self.url,
.type = typ,
.err = err,
});
return;
};
var result: js.Function.Result = undefined;
cb.tryCall(void, .{event}, &result) catch {
log.warn(.js, "script callback", .{
.url = self.url,
.type = typ,
.err = result.exception,
.stack = result.stack,
});
};
}
};

38
src/browser/js/Array.zig Normal file
View File

@@ -0,0 +1,38 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Array = @This();
js_arr: v8.Array,
context: *js.Context,
pub fn len(self: Array) usize {
return @intCast(self.js_arr.length());
}
pub fn get(self: Array, index: usize) !js.Value {
const idx_key = v8.Integer.initU32(self.context.isolate, @intCast(index));
const js_obj = self.js_arr.castTo(v8.Object);
return .{
.context = self.context,
.js_val = try js_obj.getValue(self.context.v8_context, idx_key.toValue()),
};
}

View File

@@ -392,9 +392,9 @@ pub fn createException(self: *const Context, e: v8.Value) js.Exception {
// Wrap a v8.Value, largely so that we can provide a convenient
// toString function
pub fn createValue(self: *const Context, value: v8.Value) js.Value {
pub fn createValue(self: *Context, value: v8.Value) js.Value {
return .{
.value = value,
.js_val = value,
.context = self,
};
}
@@ -665,8 +665,7 @@ pub fn mapZigInstanceToJs(self: *Context, js_obj_: ?v8.Object, value: anytype) !
pub fn jsValueToZig(self: *Context, comptime T: type, js_value: v8.Value) !T {
switch (@typeInfo(T)) {
.optional => |o| {
if (comptime o.child == js.Object) {
// If type type is a ?js.Object, then we want to pass
// If type type is a ?js.Value or a ?js.Object, then we want to pass
// a js.Object, not null. Consider a function,
// _doSomething(arg: ?Env.JsObjet) void { ... }
//
@@ -681,6 +680,14 @@ pub fn jsValueToZig(self: *Context, comptime T: type, js_value: v8.Value) !T {
// pass in `null` and the the doSomething won't
// be able to tell if `null` was explicitly passed
// or whether no parameter was passed.
if (comptime o.child == js.Value) {
return js.Value{
.context = self,
.js_val = js_value,
};
}
if (comptime o.child == js.Object) {
return js.Object{
.context = self,
.js_obj = js_value.castTo(v8.Object),
@@ -831,6 +838,16 @@ fn jsValueToStruct(self: *Context, comptime T: type, js_value: v8.Value) !?T {
return .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) };
}
if (comptime T == js.Value) {
// Caller wants an opaque js.Object. Probably a parameter
// that it needs to pass back into a callback
return js.Value{
.context = self,
.js_val = js_value,
};
}
const js_obj = js_value.castTo(v8.Object);
if (comptime T == js.Object) {

View File

@@ -135,7 +135,7 @@ pub fn isNullOrUndefined(self: Object) bool {
return self.js_obj.toValue().isNullOrUndefined();
}
pub fn nameIterator(self: Object) js.ValueIterator {
pub fn nameIterator(self: Object, allocator: Allocator) NameIterator {
const context = self.context;
const js_obj = self.js_obj;
@@ -145,6 +145,7 @@ pub fn nameIterator(self: Object) js.ValueIterator {
return .{
.count = count,
.context = context,
.allocator = allocator,
.js_obj = array.castTo(v8.Object),
};
}
@@ -153,10 +154,22 @@ pub fn toZig(self: Object, comptime T: type) !T {
return self.context.jsValueToZig(T, self.js_obj.toValue());
}
pub fn TriState(comptime T: type) type {
return union(enum) {
null: void,
undefined: void,
value: T,
};
}
pub const NameIterator = struct {
count: u32,
idx: u32 = 0,
js_obj: v8.Object,
allocator: Allocator,
context: *const Context,
pub fn next(self: *NameIterator) !?[]const u8 {
const idx = self.idx;
if (idx == self.count) {
return null;
}
self.idx += 1;
const context = self.context;
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx);
return try context.valueToString(js_val, .{ .allocator = self.allocator });
}
};

74
src/browser/js/Value.zig Normal file
View File

@@ -0,0 +1,74 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
const Value = @This();
js_val: v8.Value,
context: *js.Context,
pub fn isObject(self: Value) bool {
return self.js_val.isObject();
}
pub fn isString(self: Value) bool {
return self.js_val.isString();
}
pub fn isArray(self: Value) bool {
return self.js_val.isArray();
}
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.js_val, .{ .allocator = allocator });
}
pub fn toObject(self: Value) js.Object {
return .{
.context = self.context,
.js_obj = self.js_val.castTo(v8.Object),
};
}
pub fn toArray(self: Value) js.Array {
return .{
.context = self.context,
.js_arr = self.js_val.castTo(v8.Array),
};
}
// pub const Value = struct {
// value: v8.Value,
// context: *const Context,
// // the caller needs to deinit the string returned
// pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
// return self.context.valueToString(self.value, .{ .allocator = allocator });
// }
// pub fn fromJson(ctx: *Context, json: []const u8) !Value {
// const json_string = v8.String.initUtf8(ctx.isolate, json);
// const value = try v8.Json.parse(ctx.v8_context, json_string);
// return Value{ .context = ctx, .value = value };
// }
// };

View File

@@ -29,6 +29,8 @@ pub const Inspector = @import("Inspector.zig");
// TODO: Is "This" really necessary?
pub const This = @import("This.zig");
pub const Value = @import("Value.zig");
pub const Array = @import("Array.zig");
pub const Object = @import("Object.zig");
pub const TryCatch = @import("TryCatch.zig");
pub const Function = @import("Function.zig");
@@ -150,58 +152,6 @@ pub const Exception = struct {
}
};
pub const Value = struct {
value: v8.Value,
context: *const Context,
// the caller needs to deinit the string returned
pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.value, .{ .allocator = allocator });
}
pub fn fromJson(ctx: *Context, json: []const u8) !Value {
const json_string = v8.String.initUtf8(ctx.isolate, json);
const value = try v8.Json.parse(ctx.v8_context, json_string);
return Value{ .context = ctx, .value = value };
}
pub fn isArray(self: Value) bool {
return self.value.isArray();
}
pub fn arrayLength(self: Value) u32 {
std.debug.assert(self.value.isArray());
return self.value.castTo(v8.Array).length();
}
pub fn arrayGet(self: Value, index: u32) !Value {
std.debug.assert(self.value.isArray());
const array_obj = self.value.castTo(v8.Array).castTo(v8.Object);
const idx_key = v8.Integer.initU32(self.context.isolate, index);
const elem_val = try array_obj.getValue(self.context.v8_context, idx_key.toValue());
return self.context.createValue(elem_val);
}
};
pub const ValueIterator = struct {
count: u32,
idx: u32 = 0,
js_obj: v8.Object,
context: *const Context,
pub fn next(self: *ValueIterator) !?Value {
const idx = self.idx;
if (idx == self.count) {
return null;
}
self.idx += 1;
const context = self.context;
const js_val = try self.js_obj.getAtIndex(context.v8_context, idx);
return context.createValue(js_val);
}
};
pub fn UndefinedOr(comptime T: type) type {
return union(enum) {
undefined: void,

View File

@@ -21,7 +21,7 @@
<script id=urlSearchParams>
const inputs = [
// @ZIGDOM [["over", "9000!!"], ["abc", 123], ["key1", ""], ["key2", ""]],
// @ZIGDOM {over: "9000!!", abc: 123, key1: "", key2: ""},
{over: "9000!!", abc: 123, key1: "", key2: ""},
"over=9000!!&abc=123&key1&key2=",
"?over=9000!!&abc=123&key1&key2=",
]

View File

@@ -69,10 +69,9 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
// Read observedAttributes static property from constructor
if (constructor.getPropertyValue("observedAttributes") catch null) |observed_attrs| {
if (observed_attrs.isArray()) {
const len = observed_attrs.arrayLength();
var i: u32 = 0;
while (i < len) : (i += 1) {
const attr_val = observed_attrs.arrayGet(i) catch continue;
var js_arr = observed_attrs.toArray();
for (0..js_arr.len()) |i| {
const attr_val = js_arr.get(i) catch continue;
const attr_name = attr_val.toString(page.arena) catch continue;
const owned_attr = page.dupeString(attr_name) catch continue;
definition.observed_attributes.put(page.arena, owned_attr, {}) catch continue;

View File

@@ -34,6 +34,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const Fetch = @This();
_page: *Page,
_url: []const u8,
_buf: std.ArrayList(u8),
_response: *Response,
_resolver: js.PersistentPromiseResolver,
@@ -48,6 +49,7 @@ pub fn init(input: Input, page: *Page) !js.Promise {
fetch.* = .{
._page = page,
._buf = .empty,
._url = try page.arena.dupe(u8, request._url),
._resolver = try page.js.createPromiseResolver(.page),
._response = try Response.init(null, .{ .status = 0 }, page),
};
@@ -58,6 +60,7 @@ pub fn init(input: Input, page: *Page) !js.Promise {
if (comptime IS_DEBUG) {
log.debug(.http, "fetch", .{ .url = request._url });
}
std.debug.print("fetch: {s}\n", .{request._url});
try http_client.request(.{
.ctx = fetch,
@@ -97,6 +100,7 @@ fn httpDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
fn httpDoneCallback(ctx: *anyopaque) !void {
const self: *Fetch = @ptrCast(@alignCast(ctx));
self._response._body = self._buf.items;
std.debug.print("fetch-resolve: {s}\n", .{self._url});
return self._resolver.resolve(self._response);
}

View File

@@ -33,17 +33,26 @@ _arena: Allocator,
_params: KeyValueList,
const InitOpts = union(enum) {
value: js.Value,
query_string: []const u8,
// @ZIGMOD: Array
// @ZIGMOD: Object
};
pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {
const arena = page.arena;
const params: KeyValueList = blk: {
const opts = opts_ orelse break :blk .empty;
break :blk switch (opts) {
.query_string => |str| try paramsFromString(arena, str, &page.buf),
};
switch (opts) {
.query_string => |qs| break :blk try paramsFromString(arena, qs, &page.buf),
.value => |js_val| {
if (js_val.isObject()) {
break :blk try paramsFromObject(arena, js_val.toObject());
}
if (js_val.isString()) {
break :blk try paramsFromString(arena, try js_val.toString(arena), &page.buf);
}
return error.InvalidArgument;
}
}
};
return page._factory.create(URLSearchParams{
@@ -178,6 +187,25 @@ fn paramsFromString(allocator: Allocator, input_: []const u8, buf: []u8) !KeyVal
return params;
}
fn paramsFromObject(arena: Allocator, js_obj: js.Object) !KeyValueList {
var it = js_obj.nameIterator(arena);
var params = KeyValueList.init();
try params.ensureTotalCapacity(arena, it.count);
while (try it.next()) |name| {
const js_value = try js_obj.get(name);
const value = try js_value.toString(arena);
try params._entries.append(arena, .{
.name = try String.init(arena, name, .{}),
.value = try String.init(arena, value, .{}),
});
}
return params;
}
fn unescape(arena: Allocator, value: []const u8, buf: []u8) !String {
if (value.len == 0) {
return String.init(undefined, "", .{});