Files
browser/src/browser/webapi/element/html/Script.zig
Karl Seguin 62aa564df1 Remove Global v8::Local<V8::Context>
When we create a js.Context, we create the underlying v8.Context and store it
for the duration of the page lifetime. This works because we have a global
HandleScope - the v8.Context (which is really a v8::Local<v8::Context>) is that
to the global HandleScope, effectively making it a global.

If we want to remove our global HandleScope, then we can no longer pin the
v8.Context in our js.Context. Our js.Context now only holds a v8.Global of the
v8.Context (v8::Global<v8::Context).

This PR introduces a new type, js.Local, which takes over a lot of the
functionality previously found in either js.Caller or js.Context. The simplest
way to think about it is:

1 - For v8 -> zig calls, we create a js.Caller (as always)
2 - For zig -> v8 calls, we go through the js.Context (as always)
3 - The shared functionality, which works on a v8.Context, now belongs to js.Local

For #1 (v8 -> zig), creating a js.Local for a js.Caller is really simple and
centralized. v8 largely gives us everything we need from the
FunctionCallbackInfo or PropertyCallbackInfo.  For #2, it's messier, because we
can only create a local v8::Context if we have a HandleScope, which we may or
may not.

Unfortunately, in many cases, what to do becomes the responsibility of the caller
and much of the code has to become aware of this local-ness. What does it means
for our code? The impact is on WebAPIs that store .Global. Because the global
can't do anything. You always need to convert that .Global to a local
(e.g. js.Function.Global -> js.Function).

If you're 100% sure the WebAPI is only being invoked by a v8 callback, you can
use `page.js.local.?.toLocal(some_global).call(...)` to get the local value.

If you're 100% sure the WebAPI is only being invoked by Zig, you need to create
 `js.Local.Scope` to get access to a local:

```zig
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
ls.toLocal(some_global).call(...)
// can also access `&ls.local` for APIs that require a *const js.Local
```
For functions that can be invoked by either V8 or Zig, you should generally push
the responsibility to the caller by accepting a `local: *const js.Local`. If the
caller is a v8 callback, it can pass `page.js.local.?`. If the caller is a Zig
callback, it can create a `Local.Scope`.

As an alternative, it is possible to simply pass the *Page, and check
`if page.js.local == null` and, if so, create a Local.Scope. But this should only
be done for performance reasons. We currently only do this in 1 place, and it's
because the Zig caller doesn't know whether a Local will actually be needed and
it's potentially called on every element creating from the parser.
2026-01-19 07:28:33 +08:00

152 lines
5.0 KiB
Zig

// 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 log = @import("../../../../log.zig");
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const Script = @This();
_proto: *HtmlElement,
_src: []const u8 = "",
_on_load: ?js.Function.Global = null,
_on_error: ?js.Function.Global = null,
_executed: bool = false,
pub fn asElement(self: *Script) *Element {
return self._proto._proto;
}
pub fn asConstElement(self: *const Script) *const Element {
return self._proto._proto;
}
pub fn asNode(self: *Script) *Node {
return self.asElement().asNode();
}
pub fn getSrc(self: *const Script) []const u8 {
return self._src;
}
pub fn setSrc(self: *Script, src: []const u8, page: *Page) !void {
const element = self.asElement();
try element.setAttributeSafe("src", src, page);
self._src = element.getAttributeSafe("src") orelse unreachable;
if (element.asNode().isConnected()) {
try page.scriptAddedCallback(false, self);
}
}
pub fn getType(self: *const Script) []const u8 {
return self.asConstElement().getAttributeSafe("type") orelse "";
}
pub fn setType(self: *Script, value: []const u8, page: *Page) !void {
return self.asElement().setAttributeSafe("type", value, page);
}
pub fn getNonce(self: *const Script) []const u8 {
return self.asConstElement().getAttributeSafe("nonce") orelse "";
}
pub fn setNonce(self: *Script, value: []const u8, page: *Page) !void {
return self.asElement().setAttributeSafe("nonce", value, page);
}
pub fn getOnLoad(self: *const Script) ?js.Function.Global {
return self._on_load;
}
pub fn setOnLoad(self: *Script, cb: ?js.Function.Global) void {
self._on_load = cb;
}
pub fn getOnError(self: *const Script) ?js.Function.Global {
return self._on_error;
}
pub fn setOnError(self: *Script, cb: ?js.Function.Global) void {
self._on_error = cb;
}
pub fn getNoModule(self: *const Script) bool {
return self.asConstElement().getAttributeSafe("nomodule") != null;
}
pub fn setInnerText(self: *Script, text: []const u8, page: *Page) !void {
try self.asNode().setTextContent(text, page);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Script);
pub const Meta = struct {
pub const name = "HTMLScriptElement";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const src = bridge.accessor(Script.getSrc, Script.setSrc, .{});
pub const @"type" = bridge.accessor(Script.getType, Script.setType, .{});
pub const nonce = bridge.accessor(Script.getNonce, Script.setNonce, .{});
pub const onload = bridge.accessor(Script.getOnLoad, Script.setOnLoad, .{});
pub const onerror = bridge.accessor(Script.getOnError, Script.setOnError, .{});
pub const noModule = bridge.accessor(Script.getNoModule, null, .{});
pub const innerText = bridge.accessor(_innerText, Script.setInnerText, .{});
fn _innerText(self: *Script, page: *const Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.asNode().getTextContent(&buf.writer);
return buf.written();
}
};
pub const Build = struct {
pub fn complete(node: *Node, page: *Page) !void {
const self = node.as(Script);
const element = self.asElement();
self._src = element.getAttributeSafe("src") orelse "";
if (element.getAttributeSafe("onload")) |on_load| {
if (page.js.stringToPersistedFunction(on_load)) |func| {
self._on_load = func;
} else |err| {
log.err(.js, "script.onload", .{ .err = err, .str = on_load });
}
}
if (element.getAttributeSafe("onerror")) |on_error| {
if (page.js.stringToPersistedFunction(on_error)) |func| {
self._on_error = func;
} else |err| {
log.err(.js, "script.onerror", .{ .err = err, .str = on_error });
}
}
}
};
const testing = @import("../../../../testing.zig");
test "WebApi: Script" {
try testing.htmlRunner("element/html/script", .{});
}