Add Notification WebAPI

Adds a pretty simplistic Notification WebAPI. Also adds a dummy drawImage to
CanvasRenderingContext2D.

Trying to improve how we're seen by https://bot.sannysoft.com/
This commit is contained in:
Karl Seguin
2026-05-29 11:30:14 +08:00
parent f07eb3e264
commit 23e58b005e
6 changed files with 244 additions and 0 deletions

View File

@@ -941,6 +941,7 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/ModelContext.zig"),
@import("../webapi/Navigator.zig"),
@import("../webapi/NavigatorUAData.zig"),
@import("../webapi/Notification.zig"),
@import("../webapi/net/FormData.zig"),
@import("../webapi/net/Headers.zig"),
@import("../webapi/net/Request.zig"),

View File

@@ -158,3 +158,16 @@
testing.expectEqual(null, element.getContext('webgl'));
}
</script>
<script id="CanvasRenderingContext2D#drawImage">
{
const ctx = document.createElement('canvas').getContext('2d');
const img = new Image();
const src = document.createElement('canvas');
// No-op, but must accept the (image, dx, dy), (image, dx, dy, dw, dh) and
// 9-arg overloads without throwing.
ctx.drawImage(img, 0, 0);
ctx.drawImage(src, 0, 0, 10, 10);
testing.expectEqual(undefined, ctx.drawImage(src, 0, 0, 10, 10, 0, 0, 10, 10));
}
</script>

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<script src="./testing.js"></script>
<script id=Notification>
{
testing.expectEqual("function", typeof Notification);
testing.expectEqual("default", Notification.permission);
testing.expectEqual(2, Notification.maxActions);
}
</script>
<script id=Notification#requestPermission>
{
testing.async(async () => {
testing.expectEqual("default", await Notification.requestPermission());
});
}
</script>
<script id=Notification#constructor>
{
const n = new Notification("hello", { body: "world", icon: "i.png", tag: "t", lang: "en", dir: "rtl", silent: true });
testing.expectEqual(true, n instanceof Notification);
testing.expectEqual(true, n instanceof EventTarget);
testing.expectEqual("hello", n.title);
testing.expectEqual("world", n.body);
testing.expectEqual("i.png", n.icon);
testing.expectEqual("t", n.tag);
testing.expectEqual("en", n.lang);
testing.expectEqual("rtl", n.dir);
testing.expectEqual(true, n.silent);
testing.expectEqual("[object Notification]", Object.prototype.toString.call(n));
}
</script>
<script id=Notification#defaults>
{
const n = new Notification("only-title");
testing.expectEqual("only-title", n.title);
testing.expectEqual("", n.body);
testing.expectEqual("auto", n.dir);
testing.expectEqual(false, n.silent);
testing.expectEqual(false, n.requireInteraction);
// close() is a no-op but must exist and not throw.
n.close();
}
</script>

View File

@@ -50,6 +50,7 @@ pub const Type = union(enum) {
font_face_set: *@import("css/FontFaceSet.zig"),
websocket: *@import("net/WebSocket.zig"),
cookie_store: *@import("storage/CookieStore.zig"),
notification: *@import("Notification.zig"),
};
pub fn init(page: *Page) !*EventTarget {
@@ -161,6 +162,7 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
.font_face_set => writer.writeAll("<FontFaceSet>"),
.websocket => writer.writeAll("<WebSocket>"),
.cookie_store => writer.writeAll("<CookieStore>"),
.notification => writer.writeAll("<Notification>"),
};
}
@@ -184,6 +186,7 @@ pub fn toString(self: *EventTarget) []const u8 {
.font_face_set => return "[object FontFaceSet]",
.websocket => return "[object WebSocket]",
.cookie_store => return "[object CookieStore]",
.notification => return "[object Notification]",
};
}

View File

@@ -0,0 +1,175 @@
// 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 lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const EventTarget = @import("EventTarget.zig");
const Execution = js.Execution;
const Allocator = std.mem.Allocator;
const Notification = @This();
_rc: lp.RC(u8) = .{},
_arena: Allocator,
_proto: *EventTarget,
_title: []const u8,
_body: []const u8 = "",
_icon: []const u8 = "",
_image: []const u8 = "",
_badge: []const u8 = "",
_tag: []const u8 = "",
_lang: []const u8 = "",
_dir: []const u8 = "auto",
_silent: bool = false,
_require_interaction: bool = false,
_renotify: bool = false,
const Options = struct {
body: ?[]const u8 = null,
icon: ?[]const u8 = null,
image: ?[]const u8 = null,
badge: ?[]const u8 = null,
tag: ?[]const u8 = null,
lang: ?[]const u8 = null,
dir: ?[]const u8 = null,
silent: ?bool = null,
requireInteraction: ?bool = null,
renotify: ?bool = null,
};
pub fn init(title: []const u8, options_: ?Options, exec: *const Execution) !*Notification {
const arena = try exec.getArena(.small, "Notification");
errdefer exec.releaseArena(arena);
const options = options_ orelse Options{};
return exec._factory.eventTargetWithAllocator(arena, Notification{
._arena = arena,
._proto = undefined,
._title = try arena.dupe(u8, title),
._body = if (options.body) |v| try arena.dupe(u8, v) else "",
._icon = if (options.icon) |v| try arena.dupe(u8, v) else "",
._image = if (options.image) |v| try arena.dupe(u8, v) else "",
._badge = if (options.badge) |v| try arena.dupe(u8, v) else "",
._tag = if (options.tag) |v| try arena.dupe(u8, v) else "",
._lang = if (options.lang) |v| try arena.dupe(u8, v) else "",
._dir = if (options.dir) |d| try arena.dupe(u8, d) else "auto",
._silent = options.silent orelse false,
._require_interaction = options.requireInteraction orelse false,
._renotify = options.renotify orelse false,
});
}
pub fn deinit(self: *Notification, page: *Page) void {
page.releaseArena(self._arena);
}
pub fn releaseRef(self: *Notification, page: *Page) void {
self._rc.release(self, page);
}
pub fn acquireRef(self: *Notification) void {
self._rc.acquire();
}
pub fn close(_: *Notification) void {}
fn getPermission() []const u8 {
return "default";
}
fn getMaxActions() u32 {
return 2;
}
fn requestPermission(_: ?js.Function, exec: *const Execution) !js.Promise {
return exec.js.local.?.resolvePromise("default");
}
fn getTitle(self: *const Notification) []const u8 {
return self._title;
}
fn getBody(self: *const Notification) []const u8 {
return self._body;
}
fn getIcon(self: *const Notification) []const u8 {
return self._icon;
}
fn getImage(self: *const Notification) []const u8 {
return self._image;
}
fn getBadge(self: *const Notification) []const u8 {
return self._badge;
}
fn getTag(self: *const Notification) []const u8 {
return self._tag;
}
fn getLang(self: *const Notification) []const u8 {
return self._lang;
}
fn getDir(self: *const Notification) []const u8 {
return self._dir;
}
fn getSilent(self: *const Notification) bool {
return self._silent;
}
fn getRequireInteraction(self: *const Notification) bool {
return self._require_interaction;
}
fn getRenotify(self: *const Notification) bool {
return self._renotify;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Notification);
pub const Meta = struct {
pub const name = "Notification";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(Notification.init, .{});
pub const permission = bridge.accessor(getPermission, null, .{ .static = true });
pub const maxActions = bridge.accessor(getMaxActions, null, .{ .static = true });
pub const requestPermission = bridge.function(Notification.requestPermission, .{ .static = true });
pub const close = bridge.function(Notification.close, .{ .noop = true });
pub const title = bridge.accessor(getTitle, null, .{});
pub const body = bridge.accessor(getBody, null, .{});
pub const icon = bridge.accessor(getIcon, null, .{});
pub const image = bridge.accessor(getImage, null, .{});
pub const badge = bridge.accessor(getBadge, null, .{});
pub const tag = bridge.accessor(getTag, null, .{});
pub const lang = bridge.accessor(getLang, null, .{});
pub const dir = bridge.accessor(getDir, null, .{});
pub const silent = bridge.accessor(getSilent, null, .{});
pub const requireInteraction = bridge.accessor(getRequireInteraction, null, .{});
pub const renotify = bridge.accessor(getRenotify, null, .{});
};
const testing = @import("../../testing.zig");
test "WebApi: Notification" {
try testing.htmlRunner("notification.html", .{});
}

View File

@@ -83,6 +83,10 @@ pub fn createImageData(
pub fn putImageData(_: *const CanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}
// CanvasImageSource (HTMLImageElement, HTMLCanvasElement, ImageBitmap, ...) is
// just taken as a js.Value for now since we don't use it, and that's much easier.
pub fn drawImage(_: *const CanvasRenderingContext2D, _: js.Value, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}
pub fn getImageData(
_: *const CanvasRenderingContext2D,
_: i32, // sx
@@ -150,6 +154,7 @@ pub const JsApi = struct {
pub const createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
pub const putImageData = bridge.function(CanvasRenderingContext2D.putImageData, .{ .noop = true });
pub const drawImage = bridge.function(CanvasRenderingContext2D.drawImage, .{ .noop = true });
pub const getImageData = bridge.function(CanvasRenderingContext2D.getImageData, .{ .dom_exception = true });
pub const save = bridge.function(CanvasRenderingContext2D.save, .{ .noop = true });
pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{ .noop = true });