1 Commits

Author SHA1 Message Date
Muki Kiboigo
1dab607369 add set_pathname on URL 2025-07-09 15:35:02 -07:00
57 changed files with 872 additions and 2636 deletions

View File

@@ -17,7 +17,7 @@ inputs:
zig-v8:
description: 'zig v8 version to install'
required: false
default: 'v0.1.28'
default: 'v0.1.27'
v8:
description: 'v8 version to install'
required: false

View File

@@ -98,8 +98,6 @@ jobs:
ARCH: aarch64
OS: macos
# macos-14 runs on arm CPU. see
# https://github.com/actions/runner-images?tab=readme-ov-file
runs-on: macos-14
timeout-minutes: 15
@@ -138,12 +136,7 @@ jobs:
ARCH: x86_64
OS: macos
# macos-13 runs on x86 CPU. see
# https://github.com/actions/runner-images?tab=readme-ov-file
# If we want to build for macos-14 or superior, we need to switch to
# macos-14-large.
# No need for now, but maybe we will need it in the short term.
runs-on: macos-13
runs-on: macos-14
timeout-minutes: 15
steps:

View File

@@ -4,7 +4,7 @@ ARG MINISIG=0.12
ARG ZIG=0.14.1
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
ARG V8=13.6.233.8
ARG ZIG_V8=v0.1.28
ARG ZIG_V8=v0.1.27
ARG TARGETPLATFORM
RUN apt-get update -yq && \

View File

@@ -13,8 +13,8 @@
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
},
.v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/b22911e02e4884a76acf52aa9aff2ba169d05b40.tar.gz",
.hash = "v8-0.0.0-xddH69zCAwAzm1u5cQVa-uG5ib2y6PpENXCl8yEYdUYk",
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/dd087771378ea854452bcb010309fa9ffe5a9cac.tar.gz",
.hash = "v8-0.0.0-xddH66e8AwBL3O_A8yWQYQIyfMbKHFNVQr_NqM6YjU11",
},
//.v8 = .{ .path = "../zig-v8-fork" },
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },

View File

@@ -29,8 +29,6 @@
const Env = @import("env.zig").Env;
const parser = @import("netsurf.zig");
const DataSet = @import("html/DataSet.zig");
const ShadowRoot = @import("dom/shadow_root.zig").ShadowRoot;
const StyleSheet = @import("cssom/stylesheet.zig").StyleSheet;
const CSSStyleDeclaration = @import("cssom/css_style_declaration.zig").CSSStyleDeclaration;
// for HTMLScript (but probably needs to be added to more)
@@ -40,17 +38,10 @@ onerror: ?Env.Function = null,
// for HTMLElement
style: CSSStyleDeclaration = .empty,
dataset: ?DataSet = null,
template_content: ?*parser.DocumentFragment = null,
// For dom/element
shadow_root: ?*ShadowRoot = null,
// for html/document
ready_state: ReadyState = .loading,
// for html/HTMLStyleElement
style_sheet: ?*StyleSheet = null,
// for dom/document
active_element: ?*parser.Element = null,
@@ -69,6 +60,8 @@ active_element: ?*parser.Element = null,
// default (by returning selectedIndex == 0).
explicit_index_set: bool = false,
template_content: ?*parser.DocumentFragment = null,
const ReadyState = enum {
loading,
interactive,

View File

@@ -204,7 +204,7 @@ pub const Parser = struct {
}
const c = p.s[p.i];
if (!(nameStart(c) or c == '\\')) {
if (!nameStart(c) or c == '\\') {
return ParseError.ExpectedSelector;
}
@@ -582,7 +582,6 @@ pub const Parser = struct {
.only_of_type => return .{ .pseudo_class_only_child = true },
.input, .empty, .root, .link => return .{ .pseudo_class = pseudo_class },
.enabled, .disabled, .checked => return .{ .pseudo_class = pseudo_class },
.visible => return .{ .pseudo_class = pseudo_class },
.lang => {
if (!p.consumeParenthesis()) return ParseError.ExpectedParenthesis;
if (p.i == p.s.len) return ParseError.UnmatchParenthesis;
@@ -606,7 +605,7 @@ pub const Parser = struct {
.after, .backdrop, .before, .cue, .first_letter => return .{ .pseudo_element = pseudo_class },
.first_line, .grammar_error, .marker, .placeholder => return .{ .pseudo_element = pseudo_class },
.selection, .spelling_error => return .{ .pseudo_element = pseudo_class },
.modal, .popover_open => return .{ .pseudo_element = pseudo_class },
.modal => return .{ .pseudo_element = pseudo_class },
}
}
@@ -950,36 +949,3 @@ test "parser.parseString" {
};
}
}
test "parser.parse" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const testcases = [_]struct {
s: []const u8, // given value
exp: Selector, // expected value
err: bool = false,
}{
.{ .s = "root", .exp = .{ .tag = "root" } },
.{ .s = ".root", .exp = .{ .class = "root" } },
.{ .s = ":root", .exp = .{ .pseudo_class = .root } },
.{ .s = ".\\:bar", .exp = .{ .class = ":bar" } },
.{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } },
};
for (testcases) |tc| {
var p = Parser{ .s = tc.s, .opts = .{} };
const sel = p.parse(alloc) catch |e| {
// if error was expected, continue.
if (tc.err) continue;
std.debug.print("test case {s}\n", .{tc.s});
return e;
};
std.testing.expectEqualDeep(tc.exp, sel) catch |e| {
std.debug.print("test case {s} : {}\n", .{ tc.s, sel });
return e;
};
}
}

View File

@@ -99,8 +99,6 @@ pub const PseudoClass = enum {
selection,
spelling_error,
modal,
popover_open,
visible,
pub const Error = error{
InvalidPseudoClass,
@@ -116,108 +114,52 @@ pub const PseudoClass = enum {
}
pub fn parse(s: []const u8) Error!PseudoClass {
const longest_selector = "nth-last-of-type";
if (s.len > longest_selector.len) {
return Error.InvalidPseudoClass;
}
var buf: [longest_selector.len]u8 = undefined;
const selector = std.ascii.lowerString(&buf, s);
switch (selector.len) {
3 => switch (@as(u24, @bitCast(selector[0..3].*))) {
asUint(u24, "cue") => return .cue,
asUint(u24, "has") => return .has,
asUint(u24, "not") => return .not,
else => {},
},
4 => switch (@as(u32, @bitCast(selector[0..4].*))) {
asUint(u32, "lang") => return .lang,
asUint(u32, "link") => return .link,
asUint(u32, "root") => return .root,
else => {},
},
5 => switch (@as(u40, @bitCast(selector[0..5].*))) {
asUint(u40, "after") => return .after,
asUint(u40, "empty") => return .empty,
asUint(u40, "focus") => return .focus,
asUint(u40, "hover") => return .hover,
asUint(u40, "input") => return .input,
asUint(u40, "modal") => return .modal,
else => {},
},
6 => switch (@as(u48, @bitCast(selector[0..6].*))) {
asUint(u48, "active") => return .active,
asUint(u48, "before") => return .before,
asUint(u48, "marker") => return .marker,
asUint(u48, "target") => return .target,
else => {},
},
7 => switch (@as(u56, @bitCast(selector[0..7].*))) {
asUint(u56, "checked") => return .checked,
asUint(u56, "enabled") => return .enabled,
asUint(u56, "matches") => return .matches,
asUint(u56, "visited") => return .visited,
asUint(u56, "visible") => return .visible,
else => {},
},
8 => switch (@as(u64, @bitCast(selector[0..8].*))) {
asUint(u64, "backdrop") => return .backdrop,
asUint(u64, "contains") => return .contains,
asUint(u64, "disabled") => return .disabled,
asUint(u64, "haschild") => return .haschild,
else => {},
},
9 => switch (@as(u72, @bitCast(selector[0..9].*))) {
asUint(u72, "nth-child") => return .nth_child,
asUint(u72, "selection") => return .selection,
else => {},
},
10 => switch (@as(u80, @bitCast(selector[0..10].*))) {
asUint(u80, "first-line") => return .first_line,
asUint(u80, "last-child") => return .last_child,
asUint(u80, "matchesown") => return .matchesown,
asUint(u80, "only-child") => return .only_child,
else => {},
},
11 => switch (@as(u88, @bitCast(selector[0..11].*))) {
asUint(u88, "containsown") => return .containsown,
asUint(u88, "first-child") => return .first_child,
asUint(u88, "nth-of-type") => return .nth_of_type,
asUint(u88, "placeholder") => return .placeholder,
else => {},
},
12 => switch (@as(u96, @bitCast(selector[0..12].*))) {
asUint(u96, "first-letter") => return .first_letter,
asUint(u96, "last-of-type") => return .last_of_type,
asUint(u96, "only-of-type") => return .only_of_type,
asUint(u96, "popover-open") => return .popover_open,
else => {},
},
13 => switch (@as(u104, @bitCast(selector[0..13].*))) {
asUint(u104, "first-of-type") => return .first_of_type,
asUint(u104, "grammar-error") => return .grammar_error,
else => {},
},
14 => switch (@as(u112, @bitCast(selector[0..14].*))) {
asUint(u112, "nth-last-child") => return .nth_last_child,
asUint(u112, "spelling-error") => return .spelling_error,
else => {},
},
16 => switch (@as(u128, @bitCast(selector[0..16].*))) {
asUint(u128, "nth-last-of-type") => return .nth_last_of_type,
else => {},
},
else => {},
}
if (std.ascii.eqlIgnoreCase(s, "not")) return .not;
if (std.ascii.eqlIgnoreCase(s, "has")) return .has;
if (std.ascii.eqlIgnoreCase(s, "haschild")) return .haschild;
if (std.ascii.eqlIgnoreCase(s, "contains")) return .contains;
if (std.ascii.eqlIgnoreCase(s, "containsown")) return .containsown;
if (std.ascii.eqlIgnoreCase(s, "matches")) return .matches;
if (std.ascii.eqlIgnoreCase(s, "matchesown")) return .matchesown;
if (std.ascii.eqlIgnoreCase(s, "nth-child")) return .nth_child;
if (std.ascii.eqlIgnoreCase(s, "nth-last-child")) return .nth_last_child;
if (std.ascii.eqlIgnoreCase(s, "nth-of-type")) return .nth_of_type;
if (std.ascii.eqlIgnoreCase(s, "nth-last-of-type")) return .nth_last_of_type;
if (std.ascii.eqlIgnoreCase(s, "first-child")) return .first_child;
if (std.ascii.eqlIgnoreCase(s, "last-child")) return .last_child;
if (std.ascii.eqlIgnoreCase(s, "first-of-type")) return .first_of_type;
if (std.ascii.eqlIgnoreCase(s, "last-of-type")) return .last_of_type;
if (std.ascii.eqlIgnoreCase(s, "only-child")) return .only_child;
if (std.ascii.eqlIgnoreCase(s, "only-of-type")) return .only_of_type;
if (std.ascii.eqlIgnoreCase(s, "input")) return .input;
if (std.ascii.eqlIgnoreCase(s, "empty")) return .empty;
if (std.ascii.eqlIgnoreCase(s, "root")) return .root;
if (std.ascii.eqlIgnoreCase(s, "link")) return .link;
if (std.ascii.eqlIgnoreCase(s, "lang")) return .lang;
if (std.ascii.eqlIgnoreCase(s, "enabled")) return .enabled;
if (std.ascii.eqlIgnoreCase(s, "disabled")) return .disabled;
if (std.ascii.eqlIgnoreCase(s, "checked")) return .checked;
if (std.ascii.eqlIgnoreCase(s, "visited")) return .visited;
if (std.ascii.eqlIgnoreCase(s, "hover")) return .hover;
if (std.ascii.eqlIgnoreCase(s, "active")) return .active;
if (std.ascii.eqlIgnoreCase(s, "focus")) return .focus;
if (std.ascii.eqlIgnoreCase(s, "target")) return .target;
if (std.ascii.eqlIgnoreCase(s, "after")) return .after;
if (std.ascii.eqlIgnoreCase(s, "backdrop")) return .backdrop;
if (std.ascii.eqlIgnoreCase(s, "before")) return .before;
if (std.ascii.eqlIgnoreCase(s, "cue")) return .cue;
if (std.ascii.eqlIgnoreCase(s, "first-letter")) return .first_letter;
if (std.ascii.eqlIgnoreCase(s, "first-line")) return .first_line;
if (std.ascii.eqlIgnoreCase(s, "grammar-error")) return .grammar_error;
if (std.ascii.eqlIgnoreCase(s, "marker")) return .marker;
if (std.ascii.eqlIgnoreCase(s, "placeholder")) return .placeholder;
if (std.ascii.eqlIgnoreCase(s, "selection")) return .selection;
if (std.ascii.eqlIgnoreCase(s, "spelling-error")) return .spelling_error;
if (std.ascii.eqlIgnoreCase(s, "modal")) return .modal;
return Error.InvalidPseudoClass;
}
};
fn asUint(comptime T: type, comptime string: []const u8) T {
return @bitCast(string[0..string.len].*);
}
pub const Selector = union(enum) {
pub const Error = error{
UnknownCombinedCombinator,
@@ -569,8 +511,6 @@ pub const Selector = union(enum) {
// TODO implement using the url fragment.
// see https://developer.mozilla.org/en-US/docs/Web/CSS/:target
.target => return false,
// visible always returns true.
.visible => return true,
// all others pseudo class are handled by specialized
// pseudo_class_X selectors.

View File

@@ -1,126 +0,0 @@
// 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 Page = @import("../page.zig").Page;
const JsObject = @import("../env.zig").JsObject;
const Promise = @import("../env.zig").Promise;
const PromiseResolver = @import("../env.zig").PromiseResolver;
const Animation = @This();
effect: ?JsObject,
timeline: ?JsObject,
ready_resolver: ?PromiseResolver,
finished_resolver: ?PromiseResolver,
pub fn constructor(effect: ?JsObject, timeline: ?JsObject) !Animation {
return .{
.effect = if (effect) |eo| try eo.persist() else null,
.timeline = if (timeline) |to| try to.persist() else null,
.ready_resolver = null,
.finished_resolver = null,
};
}
pub fn get_playState(self: *const Animation) []const u8 {
_ = self;
return "finished";
}
pub fn get_pending(self: *const Animation) bool {
_ = self;
return false;
}
pub fn get_finished(self: *Animation, page: *Page) !Promise {
if (self.finished_resolver == null) {
const resolver = page.main_context.createPromiseResolver();
try resolver.resolve(self);
self.finished_resolver = resolver;
}
return self.finished_resolver.?.promise();
}
pub fn get_ready(self: *Animation, page: *Page) !Promise {
// never resolved, because we're always "finished"
if (self.ready_resolver == null) {
const resolver = page.main_context.createPromiseResolver();
self.ready_resolver = resolver;
}
return self.ready_resolver.?.promise();
}
pub fn get_effect(self: *const Animation) ?JsObject {
return self.effect;
}
pub fn set_effect(self: *Animation, effect: JsObject) !void {
self.effect = try effect.persist();
}
pub fn get_timeline(self: *const Animation) ?JsObject {
return self.timeline;
}
pub fn set_timeline(self: *Animation, timeline: JsObject) !void {
self.timeline = try timeline.persist();
}
pub fn _play(self: *const Animation) void {
_ = self;
}
pub fn _pause(self: *const Animation) void {
_ = self;
}
pub fn _cancel(self: *const Animation) void {
_ = self;
}
pub fn _finish(self: *const Animation) void {
_ = self;
}
pub fn _reverse(self: *const Animation) void {
_ = self;
}
const testing = @import("../../testing.zig");
test "Browser.Animation" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
defer runner.deinit();
try runner.testCases(&.{
.{ "let a1 = document.createElement('div').animate(null, null)", null },
.{ "a1.playState", "finished" },
.{ "let cb = [];", null },
.{ "a1.ready.then(() => { cb.push('ready') })", null },
.{
\\ a1.finished.then((x) => {
\\ cb.push('finished');
\\ cb.push(x == a1);
\\ })
,
null,
},
.{ "cb", "finished,true" },
}, .{});
}

View File

@@ -1,361 +0,0 @@
// 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 parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler;
const JsObject = Env.JsObject;
const Function = Env.Function;
const Allocator = std.mem.Allocator;
const MAX_QUEUE_SIZE = 10;
pub const Interfaces = .{ MessageChannel, MessagePort };
const MessageChannel = @This();
port1: *MessagePort,
port2: *MessagePort,
pub fn constructor(page: *Page) !MessageChannel {
// Why do we allocate this rather than storing directly in the struct?
// https://github.com/lightpanda-io/project/discussions/165
const port1 = try page.arena.create(MessagePort);
const port2 = try page.arena.create(MessagePort);
port1.* = .{
.pair = port2,
};
port2.* = .{
.pair = port1,
};
return .{
.port1 = port1,
.port2 = port2,
};
}
pub fn get_port1(self: *const MessageChannel) *MessagePort {
return self.port1;
}
pub fn get_port2(self: *const MessageChannel) *MessagePort {
return self.port2;
}
pub const MessagePort = struct {
pub const prototype = *EventTarget;
proto: parser.EventTargetTBase = .{ .internal_target_type = .message_port },
pair: *MessagePort,
closed: bool = false,
started: bool = false,
onmessage_cbk: ?Function = null,
onmessageerror_cbk: ?Function = null,
// This is the queue of messages to dispatch to THIS MessagePort when the
// MessagePort is started.
queue: std.ArrayListUnmanaged(JsObject) = .empty,
pub const PostMessageOption = union(enum) {
transfer: JsObject,
options: Opts,
pub const Opts = struct {
transfer: JsObject,
};
};
pub fn _postMessage(self: *MessagePort, obj: JsObject, opts_: ?PostMessageOption, page: *Page) !void {
if (self.closed) {
return;
}
if (opts_ != null) {
log.warn(.web_api, "not implemented", .{ .feature = "MessagePort postMessage options" });
return error.NotImplemented;
}
try self.pair.dispatchOrQueue(obj, page.arena);
}
// Start impacts the ability to receive a message.
// Given pair1 (started) and pair2 (not started), then:
// pair2.postMessage('x'); //will be dispatched to pair1.onmessage
// pair1.postMessage('x'); // will be queued until pair2 is started
pub fn _start(self: *MessagePort) !void {
if (self.started) {
return;
}
self.started = true;
for (self.queue.items) |data| {
try self.dispatch(data);
}
// we'll never use this queue again, but it's allocated with an arena
// we don't even need to clear it, but it seems a bit safer to do at
// least that
self.queue.clearRetainingCapacity();
}
// Closing seems to stop both the publishing and receiving of messages,
// effectively rendering the channel useless. It cannot be reversed.
pub fn _close(self: *MessagePort) void {
self.closed = true;
self.pair.closed = true;
}
pub fn get_onmessage(self: *MessagePort) ?Function {
return self.onmessage_cbk;
}
pub fn get_onmessageerror(self: *MessagePort) ?Function {
return self.onmessageerror_cbk;
}
pub fn set_onmessage(self: *MessagePort, listener: EventHandler.Listener, page: *Page) !void {
if (self.onmessage_cbk) |cbk| {
try self.unregister("message", cbk.id);
}
self.onmessage_cbk = try self.register(page.arena, "message", listener);
// When onmessage is set directly, then it's like start() was called.
// If addEventListener('message') is used, the app has to call start()
// explicitly.
try self._start();
}
pub fn set_onmessageerror(self: *MessagePort, listener: EventHandler.Listener, page: *Page) !void {
if (self.onmessageerror_cbk) |cbk| {
try self.unregister("messageerror", cbk.id);
}
self.onmessageerror_cbk = try self.register(page.arena, "messageerror", listener);
}
// called from our pair. If port1.postMessage("x") is called, then this
// will be called on port2.
fn dispatchOrQueue(self: *MessagePort, obj: JsObject, arena: Allocator) !void {
// our pair should have checked this already
std.debug.assert(self.closed == false);
if (self.started) {
return self.dispatch(try obj.persist());
}
if (self.queue.items.len > MAX_QUEUE_SIZE) {
// This isn't part of the spec, but not putting a limit is reckless
return error.MessageQueueLimit;
}
return self.queue.append(arena, try obj.persist());
}
fn dispatch(self: *MessagePort, obj: JsObject) !void {
// obj is already persisted, don't use `MessageEvent.constructor`, but
// go directly to `init`, which assumes persisted objects.
var evt = try MessageEvent.init(.{ .data = obj });
_ = try parser.eventTargetDispatchEvent(
parser.toEventTarget(MessagePort, self),
@as(*parser.Event, @ptrCast(&evt)),
);
}
fn register(
self: *MessagePort,
alloc: Allocator,
typ: []const u8,
listener: EventHandler.Listener,
) !?Function {
const target = @as(*parser.EventTarget, @ptrCast(self));
const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable;
return eh.callback;
}
fn unregister(self: *MessagePort, typ: []const u8, cbk_id: usize) !void {
const et = @as(*parser.EventTarget, @ptrCast(self));
const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id);
if (lst == null) {
return;
}
try parser.eventTargetRemoveEventListener(et, typ, lst.?, false);
}
};
pub const MessageEvent = struct {
const Event = @import("../events/event.zig").Event;
const DOMException = @import("exceptions.zig").DOMException;
pub const prototype = *Event;
pub const Exception = DOMException;
pub const union_make_copy = true;
proto: parser.Event,
data: ?JsObject,
// You would think if port1 sends to port2, the source would be port2
// (which is how I read the documentation), but it appears to always be
// null. It can always be set explicitly via the constructor;
source: ?JsObject,
origin: []const u8,
// This is used for Server-Sent events. Appears to always be an empty
// string for MessagePort messages.
last_event_id: []const u8,
// This might be related to the "transfer" option of postMessage which
// we don't yet support. For "normal" message, it's always an empty array.
// Though it could be set explicitly via the constructor
ports: []*MessagePort,
const Options = struct {
data: ?JsObject = null,
source: ?JsObject = null,
origin: []const u8 = "",
lastEventId: []const u8 = "",
ports: []*MessagePort = &.{},
};
pub fn constructor(opts: Options) !MessageEvent {
return init(.{
.data = if (opts.data) |obj| try obj.persist() else null,
.source = if (opts.source) |obj| try obj.persist() else null,
.ports = opts.ports,
.origin = opts.origin,
.lastEventId = opts.lastEventId,
});
}
// This is like "constructor", but it assumes JsObjects have already been
// persisted. Necessary because this `new MessageEvent()` can be called
// directly from JS OR from a port.postMessage. In the latter case, data
// may have already been persisted (as it might need to be queued);
fn init(opts: Options) !MessageEvent {
const event = try parser.eventCreate();
defer parser.eventDestroy(event);
try parser.eventInit(event, "message", .{});
try parser.eventSetInternalType(event, .message_event);
return .{
.proto = event.*,
.data = opts.data,
.source = opts.source,
.ports = opts.ports,
.origin = opts.origin,
.last_event_id = opts.lastEventId,
};
}
pub fn get_data(self: *const MessageEvent) !?JsObject {
return self.data;
}
pub fn get_origin(self: *const MessageEvent) []const u8 {
return self.origin;
}
pub fn get_source(self: *const MessageEvent) ?JsObject {
return self.source;
}
pub fn get_ports(self: *const MessageEvent) []*MessagePort {
return self.ports;
}
pub fn get_lastEventId(self: *const MessageEvent) []const u8 {
return self.last_event_id;
}
};
const testing = @import("../../testing.zig");
test "Browser.MessageChannel" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{
.html = "",
});
defer runner.deinit();
try runner.testCases(&.{
.{ "const mc1 = new MessageChannel()", null },
.{ "mc1.port1 == mc1.port1", "true" },
.{ "mc1.port2 == mc1.port2", "true" },
.{ "mc1.port1 != mc1.port2", "true" },
.{ "mc1.port1.postMessage('msg1');", "undefined" },
.{
\\ let message = null;
\\ let target = null;
\\ let currentTarget = null;
\\ mc1.port2.onmessage = (e) => {
\\ message = e.data;
\\ target = e.target;
\\ currentTarget = e.currentTarget;
\\ };
,
null,
},
// as soon as onmessage is called, queued messages are delivered
.{ "message", "msg1" },
.{ "target == mc1.port2", "true" },
.{ "currentTarget == mc1.port2", "true" },
.{ "mc1.port1.postMessage('msg2');", "undefined" },
.{ "message", "msg2" },
.{ "target == mc1.port2", "true" },
.{ "currentTarget == mc1.port2", "true" },
.{ "message = null", null },
.{ "mc1.port1.close();", null },
.{ "mc1.port1.postMessage('msg3');", "undefined" },
.{ "message", "null" },
}, .{});
try runner.testCases(&.{
.{ "const mc2 = new MessageChannel()", null },
.{ "mc2.port2.postMessage('msg1');", "undefined" },
.{ "mc2.port1.postMessage('msg2');", "undefined" },
.{
\\ let message1 = null;
\\ mc2.port1.addEventListener('message', (e) => {
\\ message1 = e.data;
\\ });
,
null,
},
.{
\\ let message2 = null;
\\ mc2.port2.addEventListener('message', (e) => {
\\ message2 = e.data;
\\ });
,
null,
},
.{ "message1", "null" },
.{ "message2", "null" },
.{ "mc2.port2.start()", null },
.{ "message1", "null" },
.{ "message2", "msg2" },
.{ "message2 = null", null },
.{ "mc2.port1.start()", null },
.{ "message1", "msg1" },
.{ "message2", "null" },
}, .{});
}

View File

@@ -32,8 +32,6 @@ const css = @import("css.zig");
const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union;
const TreeWalker = @import("tree_walker.zig").TreeWalker;
const CSSStyleSheet = @import("../cssom/css_stylesheet.zig").CSSStyleSheet;
const NodeIterator = @import("node_iterator.zig").NodeIterator;
const Range = @import("range.zig").Range;
const Env = @import("../env.zig").Env;
@@ -124,11 +122,28 @@ pub const Document = struct {
return try Element.toInterface(e);
}
pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
// The elements namespace is the HTML namespace when document is an HTML document
// https://dom.spec.whatwg.org/#ref-for-dom-document-createelement%E2%91%A0
const e = try parser.documentCreateElementNS(self, "http://www.w3.org/1999/xhtml", tag_name);
return Element.toInterface(e);
const CreateElementResult = union(enum) {
element: ElementUnion,
custom: Env.JsObject,
};
pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !CreateElementResult {
const custom_element = page.window.custom_elements._get(tag_name) orelse {
const e = try parser.documentCreateElement(self, tag_name);
return .{ .element = try Element.toInterface(e) };
};
var result: Env.Function.Result = undefined;
const js_obj = custom_element.newInstance(&result) catch |err| {
log.fatal(.user_script, "newInstance error", .{
.err = result.exception,
.stack = result.stack,
.tag_name = tag_name,
.source = "createElement",
});
return err;
};
return .{ .custom = js_obj };
}
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {
@@ -249,10 +264,6 @@ pub const Document = struct {
return try TreeWalker.init(root, what_to_show, filter);
}
pub fn _createNodeIterator(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?NodeIterator.NodeIteratorOpts) !NodeIterator {
return try NodeIterator.init(root, what_to_show, filter);
}
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| {
if (state.active_element) |ae| {
@@ -284,11 +295,6 @@ pub const Document = struct {
pub fn _createRange(_: *parser.Document, page: *Page) Range {
return Range.constructor(page);
}
// TODO: dummy implementation
pub fn get_styleSheets(_: *parser.Document) []CSSStyleSheet {
return &.{};
}
};
const testing = @import("../../testing.zig");
@@ -457,9 +463,6 @@ test "Browser.DOM.Document" {
,
"1",
},
.{ "document.querySelectorAll('.\\\\:popover-open').length", "0" },
.{ "document.querySelectorAll('.foo\\\\:bar').length", "0" },
}, .{});
try runner.testCases(&.{
@@ -468,10 +471,6 @@ test "Browser.DOM.Document" {
.{ "document.activeElement === document.getElementById('link')", "true" },
}, .{});
try runner.testCases(&.{
.{ "document.styleSheets.length", "0" },
}, .{});
// this test breaks the doc structure, keep it at the end of the test
// suite.
try runner.testCases(&.{

View File

@@ -22,7 +22,6 @@ const Page = @import("../page.zig").Page;
const NodeList = @import("nodelist.zig").NodeList;
const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union;
const collection = @import("html_collection.zig");
const Node = @import("node.zig").Node;
@@ -72,15 +71,6 @@ pub const DocumentFragment = struct {
pub fn _querySelectorAll(self: *parser.DocumentFragment, selector: []const u8, page: *Page) !NodeList {
return css.querySelectorAll(page.arena, parser.documentFragmentToNode(self), selector);
}
pub fn get_childElementCount(self: *parser.DocumentFragment) !u32 {
var children = try get_children(self);
return children.get_length();
}
pub fn get_children(self: *parser.DocumentFragment) !collection.HTMLCollection {
return collection.HTMLCollectionChildren(parser.documentFragmentToNode(self), false);
}
};
const testing = @import("../../testing.zig");
@@ -103,13 +93,10 @@ test "Browser.DOM.DocumentFragment" {
try runner.testCases(&.{
.{ "let f = document.createDocumentFragment()", null },
.{ "let d = document.createElement('div');", null },
.{ "d.childElementCount", "0" },
.{ "d.id = 'x';", null },
.{ "document.getElementById('x') == null;", "true" },
.{ "f.append(d);", null },
.{ "f.childElementCount", "1" },
.{ "f.children[0].id", "x" },
.{ "document.getElementById('x') == null;", "true" },
.{ "document.getElementsByTagName('body')[0].append(f.cloneNode(true));", null },

View File

@@ -28,7 +28,6 @@ const MutationObserver = @import("mutation_observer.zig");
const IntersectionObserver = @import("intersection_observer.zig");
const DOMParser = @import("dom_parser.zig").DOMParser;
const TreeWalker = @import("tree_walker.zig").TreeWalker;
const NodeIterator = @import("node_iterator.zig").NodeIterator;
const NodeFilter = @import("node_filter.zig").NodeFilter;
const PerformanceObserver = @import("performance_observer.zig").PerformanceObserver;
@@ -47,11 +46,8 @@ pub const Interfaces = .{
IntersectionObserver.Interfaces,
DOMParser,
TreeWalker,
NodeIterator,
NodeFilter,
@import("performance.zig").Interfaces,
PerformanceObserver,
@import("range.zig").Interfaces,
@import("Animation.zig"),
@import("MessageChannel.zig").Interfaces,
};

View File

@@ -30,11 +30,6 @@ const Node = @import("node.zig").Node;
const Walker = @import("walker.zig").WalkerDepthFirst;
const NodeList = @import("nodelist.zig").NodeList;
const HTMLElem = @import("../html/elements.zig");
const ShadowRoot = @import("../dom/shadow_root.zig").ShadowRoot;
const Animation = @import("Animation.zig");
const JsObject = @import("../env.zig").JsObject;
pub const Union = @import("../html/elements.zig").Union;
// WEB IDL https://dom.spec.whatwg.org/#element
@@ -113,13 +108,13 @@ pub const Element = struct {
pub fn get_innerHTML(self: *parser.Element, page: *Page) ![]const u8 {
var buf = std.ArrayList(u8).init(page.arena);
try dump.writeChildren(parser.elementToNode(self), .{}, buf.writer());
try dump.writeChildren(parser.elementToNode(self), buf.writer());
return buf.items;
}
pub fn get_outerHTML(self: *parser.Element, page: *Page) ![]const u8 {
var buf = std.ArrayList(u8).init(page.arena);
try dump.writeNode(parser.elementToNode(self), .{}, buf.writer());
try dump.writeNode(parser.elementToNode(self), buf.writer());
return buf.items;
}
@@ -132,40 +127,16 @@ pub const Element = struct {
// remove existing children
try Node.removeChildren(node);
// I'm not sure what the exact behavior is supposed to be. Initially,
// we were only copying the body of the document fragment. But it seems
// like head elements should be copied too. Specifically, some sites
// create script tags via innerHTML, which we need to capture.
// If you play with this in a browser, you should notice that the
// behavior is different depending on whether you're in a blank page
// or an actual document. In a blank page, something like:
// x.innerHTML = '<script></script>';
// does _not_ create an empty script, but in a real page, it does. Weird.
const fragment_node = parser.documentFragmentToNode(fragment);
const html = try parser.nodeFirstChild(fragment_node) orelse return;
const head = try parser.nodeFirstChild(html) orelse return;
{
// First, copy some of the head element
const children = try parser.nodeGetChildNodes(head);
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(node, child);
}
}
// get fragment body children
const children = try parser.documentFragmentBodyChildren(fragment) orelse return;
{
const body = try parser.nodeNextSibling(head) orelse return;
const children = try parser.nodeGetChildNodes(body);
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because nodeAppendChild moves the node out of
// the nodeList and into the new tree
const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(node, child);
}
// append children to the node
const ln = try parser.nodeListLength(children);
for (0..ln) |_| {
// always index 0, because ndoeAppendChild moves the node out of
// the nodeList and into the new tree
const child = try parser.nodeListItem(children, 0) orelse continue;
_ = try parser.nodeAppendChild(node, child);
}
}
@@ -189,14 +160,8 @@ pub const Element = struct {
}
}
// don't use parser.nodeHasAttributes(...) because that returns true/false
// based on the type, e.g. a node never as attributes, an element always has
// attributes. But, Element.hasAttributes is supposed to return true only
// if the element has at least 1 attribute.
pub fn _hasAttributes(self: *parser.Element) !bool {
// an element _must_ have at least an empty attribute
const node_map = try parser.nodeGetAttributes(parser.elementToNode(self)) orelse unreachable;
return try parser.namedNodeMapGetLength(node_map) > 0;
return try parser.nodeHasAttributes(parser.elementToNode(self));
}
pub fn _getAttribute(self: *parser.Element, qname: []const u8) !?[]const u8 {
@@ -464,60 +429,6 @@ pub const Element = struct {
_ = opts;
return true;
}
const AttachShadowOpts = struct {
mode: []const u8, // must be specified
};
pub fn _attachShadow(self: *parser.Element, opts: AttachShadowOpts, page: *Page) !*ShadowRoot {
const mode = std.meta.stringToEnum(ShadowRoot.Mode, opts.mode) orelse return error.InvalidArgument;
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
if (state.shadow_root) |sr| {
if (mode != sr.mode) {
// this is the behavior per the spec
return error.NotSupportedError;
}
try Node.removeChildren(@alignCast(@ptrCast(sr.proto)));
return sr;
}
// Not sure what to do if there is no owner document
const doc = try parser.nodeOwnerDocument(@ptrCast(self)) orelse return error.InvalidArgument;
const fragment = try parser.documentCreateDocumentFragment(doc);
const sr = try page.arena.create(ShadowRoot);
sr.* = .{
.host = self,
.mode = mode,
.proto = fragment,
};
state.shadow_root = sr;
return sr;
}
pub fn get_shadowRoot(self: *parser.Element, page: *Page) ?*ShadowRoot {
const state = page.getNodeState(@alignCast(@ptrCast(self))) orelse return null;
const sr = state.shadow_root orelse return null;
if (sr.mode == .closed) {
return null;
}
return sr;
}
pub fn _animate(self: *parser.Element, effect: JsObject, opts: JsObject) !Animation {
_ = self;
_ = opts;
return Animation.constructor(effect, null);
}
pub fn _remove(self: *parser.Element) !void {
// TODO: This hasn't been tested to make sure all references to this
// node are properly updated. A lot of libdom is lazy and will look
// for related elements JIT by walking the tree, but there could be
// cases in libdom or the Zig WebAPI where this reference is kept
const as_node: *parser.Node = @ptrCast(self);
const parent = try parser.nodeParentNode(as_node) orelse return;
_ = try Node._removeChild(parent, as_node);
}
};
// Tests
@@ -768,22 +679,4 @@ test "Browser.DOM.Element" {
.{ "div1.innerHTML = \" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>\"", null },
.{ "div1.getElementsByTagName('a').length", "1" },
}, .{});
try runner.testCases(&.{
.{ "document.createElement('a').hasAttributes()", "false" },
.{ "var fc; (fc = document.createElement('div')).innerHTML = '<script><\\/script>'", null },
.{ "fc.outerHTML", "<div><script></script></div>" },
.{ "fc; (fc = document.createElement('div')).innerHTML = '<script><\\/script><p>hello</p>'", null },
.{ "fc.outerHTML", "<div><script></script><p>hello</p></div>" },
}, .{});
try runner.testCases(&.{
.{ "const rm = document.createElement('div')", null },
.{ "rm.id = 'to-remove'", null },
.{ "document.getElementsByTagName('body')[0].appendChild(rm)", null },
.{ "document.getElementById('to-remove') != null", "true" },
.{ "rm.remove()", "undefined" },
.{ "document.getElementById('to-remove') != null", "false" },
}, .{});
}

View File

@@ -16,7 +16,6 @@
// 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 Env = @import("../env.zig").Env;
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
@@ -29,8 +28,6 @@ const nod = @import("node.zig");
pub const Union = union(enum) {
node: nod.Union,
xhr: *@import("../xhr/xhr.zig").XMLHttpRequest,
plain: *parser.EventTarget,
message_port: *@import("MessageChannel.zig").MessagePort,
};
// EventTarget implementation
@@ -38,46 +35,34 @@ pub const EventTarget = struct {
pub const Self = parser.EventTarget;
pub const Exception = DOMException;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .plain },
pub fn toInterface(et: *parser.EventTarget, page: *Page) !Union {
pub fn toInterface(e: *parser.Event, et: *parser.EventTarget, page: *Page) !Union {
// libdom assumes that all event targets are libdom nodes. They are not.
switch (try parser.eventTargetInternalType(et)) {
.libdom_node => {
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
},
.plain => return .{ .plain = et },
// The window is a common non-node target, but it's easy to handle as
// its a singleton.
if (@intFromPtr(et) == @intFromPtr(&page.window.base)) {
return .{ .node = .{ .Window = &page.window } };
}
// AbortSignal is another non-node target. It has a distinct usage though
// so we hijack the event internal type to identity if.
switch (try parser.eventGetInternalType(e)) {
.abort_signal => {
// AbortSignal is a special case, it has its own internal type.
// We return it as a node, but we need to handle it differently.
return .{ .node = .{ .AbortSignal = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) } };
},
.window => {
// The window is a common non-node target, but it's easy to handle as its a singleton.
std.debug.assert(@intFromPtr(et) == @intFromPtr(&page.window.base));
return .{ .node = .{ .Window = &page.window } };
},
.xhr => {
.xhr_event => {
const XMLHttpRequestEventTarget = @import("../xhr/event_target.zig").XMLHttpRequestEventTarget;
const base: *XMLHttpRequestEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
return .{ .xhr = @fieldParentPtr("proto", base) };
},
.message_port => {
return .{ .message_port = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) };
else => {
return .{ .node = try nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))) };
},
else => return error.MissingEventTargetType,
}
}
// JS funcs
// --------
pub fn constructor(page: *Page) !*parser.EventTarget {
const et = try page.arena.create(EventTarget);
return @ptrCast(&et.base);
}
pub fn _addEventListener(
self: *parser.EventTarget,
typ: []const u8,
@@ -143,10 +128,6 @@ test "Browser.DOM.EventTarget" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "new EventTarget()", "[object EventTarget]" },
}, .{});
try runner.testCases(&.{
.{ "let content = document.getElementById('content')", "undefined" },
.{ "let para = document.getElementById('para')", "undefined" },

View File

@@ -17,15 +17,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Node = @import("node.zig").Node;
pub const NodeFilter = struct {
pub const _FILTER_ACCEPT: u16 = 1;
pub const _FILTER_REJECT: u16 = 2;
pub const _FILTER_SKIP: u16 = 3;
pub const _SHOW_ALL: u32 = std.math.maxInt(u32);
pub const _SHOW_ELEMENT: u32 = 0b1;
pub const _SHOW_ATTRIBUTE: u32 = 0b10;
@@ -41,39 +37,6 @@ pub const NodeFilter = struct {
pub const _SHOW_NOTATION: u32 = 0b100000000000;
};
const VerifyResult = enum { accept, skip, reject };
pub fn verify(what_to_show: u32, filter: ?Env.Function, node: *parser.Node) !VerifyResult {
const node_type = try parser.nodeType(node);
// Verify that we can show this node type.
if (!switch (node_type) {
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
.comment => what_to_show & NodeFilter._SHOW_COMMENT != 0,
.document => what_to_show & NodeFilter._SHOW_DOCUMENT != 0,
.document_fragment => what_to_show & NodeFilter._SHOW_DOCUMENT_FRAGMENT != 0,
.document_type => what_to_show & NodeFilter._SHOW_DOCUMENT_TYPE != 0,
.element => what_to_show & NodeFilter._SHOW_ELEMENT != 0,
.entity => what_to_show & NodeFilter._SHOW_ENTITY != 0,
.entity_reference => what_to_show & NodeFilter._SHOW_ENTITY_REFERENCE != 0,
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
.text => what_to_show & NodeFilter._SHOW_TEXT != 0,
}) return .reject;
// Verify that we aren't filtering it out.
if (filter) |f| {
const acceptance = try f.call(u16, .{try Node.toInterface(node)});
return switch (acceptance) {
NodeFilter._FILTER_ACCEPT => .accept,
NodeFilter._FILTER_REJECT => .reject,
NodeFilter._FILTER_SKIP => .skip,
else => .reject,
};
} else return .accept;
}
const testing = @import("../../testing.zig");
test "Browser.DOM.NodeFilter" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});

View File

@@ -1,290 +0,0 @@
// 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 parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const NodeFilter = @import("node_filter.zig");
const Node = @import("node.zig").Node;
const NodeUnion = @import("node.zig").Union;
// https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator
// While this is similar to TreeWalker it has its own implementation as there are several subtle differences
// For example:
// - nextNode returns the reference node, whereas TreeWalker returns the next node
// - Skip and reject are equivalent for NodeIterator, for TreeWalker they are different
pub const NodeIterator = struct {
root: *parser.Node,
reference_node: *parser.Node,
what_to_show: u32,
filter: ?NodeIteratorOpts,
filter_func: ?Env.Function,
pointer_before_current: bool = true,
pub const NodeIteratorOpts = union(enum) {
function: Env.Function,
object: struct { acceptNode: Env.Function },
};
pub fn init(node: *parser.Node, what_to_show: ?u32, filter: ?NodeIteratorOpts) !NodeIterator {
var filter_func: ?Env.Function = null;
if (filter) |f| {
filter_func = switch (f) {
.function => |func| func,
.object => |o| o.acceptNode,
};
}
return .{
.root = node,
.reference_node = node,
.what_to_show = what_to_show orelse NodeFilter.NodeFilter._SHOW_ALL,
.filter = filter,
.filter_func = filter_func,
};
}
pub fn get_filter(self: *const NodeIterator) ?NodeIteratorOpts {
return self.filter;
}
pub fn get_pointerBeforeReferenceNode(self: *const NodeIterator) bool {
return self.pointer_before_current;
}
pub fn get_referenceNode(self: *const NodeIterator) !NodeUnion {
return try Node.toInterface(self.reference_node);
}
pub fn get_root(self: *const NodeIterator) !NodeUnion {
return try Node.toInterface(self.root);
}
pub fn get_whatToShow(self: *const NodeIterator) u32 {
return self.what_to_show;
}
pub fn _nextNode(self: *NodeIterator) !?NodeUnion {
if (self.pointer_before_current) { // Unlike TreeWalker, NodeIterator starts at the first node
self.pointer_before_current = false;
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
return try Node.toInterface(self.reference_node);
}
}
if (try self.firstChild(self.reference_node)) |child| {
self.reference_node = child;
return try Node.toInterface(child);
}
var current = self.reference_node;
while (current != self.root) {
if (try self.nextSibling(current)) |sibling| {
self.reference_node = sibling;
return try Node.toInterface(sibling);
}
current = (try parser.nodeParentNode(current)) orelse break;
}
return null;
}
pub fn _previousNode(self: *NodeIterator) !?NodeUnion {
if (!self.pointer_before_current) {
self.pointer_before_current = true;
if (.accept == try NodeFilter.verify(self.what_to_show, self.filter_func, self.reference_node)) {
return try Node.toInterface(self.reference_node); // Still need to verify as last may be first as well
}
}
if (self.reference_node == self.root) return null;
var current = self.reference_node;
while (try parser.nodePreviousSibling(current)) |previous| {
current = previous;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => {
// Get last child if it has one.
if (try self.lastChild(current)) |child| {
self.reference_node = child;
return try Node.toInterface(child);
}
// Otherwise, this node is our previous one.
self.reference_node = current;
return try Node.toInterface(current);
},
.reject, .skip => {
// Get last child if it has one.
if (try self.lastChild(current)) |child| {
self.reference_node = child;
return try Node.toInterface(child);
}
},
}
}
if (current != self.root) {
if (try self.parentNode(current)) |parent| {
self.reference_node = parent;
return try Node.toInterface(parent);
}
}
return null;
}
fn firstChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node);
const child_count = try parser.nodeListLength(children);
for (0..child_count) |i| {
const index: u32 = @intCast(i);
const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
.reject, .skip => if (try self.firstChild(child)) |gchild| return gchild,
}
}
return null;
}
fn lastChild(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
const children = try parser.nodeGetChildNodes(node);
const child_count = try parser.nodeListLength(children);
var index: u32 = child_count;
while (index > 0) {
index -= 1;
const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
.accept => return child, // NOTE: Skip and reject are equivalent for NodeIterator, this is different from TreeWalker
.reject, .skip => if (try self.lastChild(child)) |gchild| return gchild,
}
}
return null;
}
// This implementation is actually the same as :TreeWalker
fn parentNode(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
if (self.root == node) return null;
var current = node;
while (true) {
if (current == self.root) return null;
current = (try parser.nodeParentNode(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current,
.reject, .skip => continue,
}
}
}
// This implementation is actually the same as :TreeWalker
fn nextSibling(self: *const NodeIterator, node: *parser.Node) !?*parser.Node {
var current = node;
while (true) {
current = (try parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
.accept => return current,
.skip, .reject => continue,
}
}
return null;
}
};
const testing = @import("../../testing.zig");
test "Browser.DOM.NodeFilter" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{
\\ const nodeIterator = document.createNodeIterator(
\\ document.body,
\\ NodeFilter.SHOW_ELEMENT,
\\ {
\\ acceptNode(node) {
\\ return NodeFilter.FILTER_ACCEPT;
\\ },
\\ },
\\ );
\\ nodeIterator.nextNode().nodeName;
,
"BODY",
},
.{ "nodeIterator.nextNode().nodeName", "DIV" },
.{ "nodeIterator.nextNode().nodeName", "A" },
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
.{ "nodeIterator.nextNode().nodeName", "A" }, // pointer_before_current flips
.{ "nodeIterator.previousNode().nodeName", "A" }, // pointer_before_current flips
.{ "nodeIterator.previousNode().nodeName", "DIV" },
.{ "nodeIterator.previousNode().nodeName", "BODY" },
.{ "nodeIterator.previousNode()", "null" }, // Not HEAD since body is root
.{ "nodeIterator.previousNode()", "null" }, // Keeps returning null
.{ "nodeIterator.nextNode().nodeName", "BODY" },
.{ "nodeIterator.nextNode().nodeName", null },
.{ "nodeIterator.nextNode().nodeName", null },
.{ "nodeIterator.nextNode().nodeName", null },
.{ "nodeIterator.nextNode().nodeName", "SPAN" },
.{ "nodeIterator.nextNode().nodeName", "P" },
.{ "nodeIterator.nextNode()", "null" }, // Just the last one
.{ "nodeIterator.nextNode()", "null" }, // Keeps returning null
.{ "nodeIterator.previousNode().nodeName", "P" },
}, .{});
try runner.testCases(&.{
.{
\\ const notationIterator = document.createNodeIterator(
\\ document.body,
\\ NodeFilter.SHOW_NOTATION,
\\ );
\\ notationIterator.nextNode();
,
"null",
},
.{ "notationIterator.previousNode()", "null" },
}, .{});
try runner.testCases(&.{
.{ "nodeIterator.filter.acceptNode(document.body)", "1" },
.{ "notationIterator.filter", "null" },
.{
\\ const rejectIterator = document.createNodeIterator(
\\ document.body,
\\ NodeFilter.SHOW_ALL,
\\ (e => { return NodeFilter.FILTER_REJECT}),
\\ );
\\ rejectIterator.filter(document.body);
,
"2",
},
}, .{});
}

View File

@@ -29,12 +29,17 @@ pub const Interfaces = .{
PerformanceMark,
};
const MarkOptions = struct {
detail: ?Env.JsObject = null,
start_time: ?f64 = null,
};
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
pub const Performance = struct {
pub const prototype = *EventTarget;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .performance },
base: parser.EventTargetTBase = parser.EventTargetTBase{},
time_origin: std.time.Timer,
// if (Window.crossOriginIsolated) -> Resolution in isolated contexts: 5 microseconds
@@ -61,21 +66,11 @@ pub const Performance = struct {
return limitedResolutionMs(self.time_origin.read());
}
pub fn _mark(_: *Performance, name: []const u8, _options: ?PerformanceMark.Options, page: *Page) !PerformanceMark {
pub fn _mark(_: *Performance, name: []const u8, _options: ?MarkOptions, page: *Page) !PerformanceMark {
const mark: PerformanceMark = try .constructor(name, _options, page);
// TODO: Should store this in an entries list
return mark;
}
// TODO: fn _mark should record the marks in a lookup
pub fn _clearMarks(_: *Performance, name: ?[]const u8) void {
_ = name;
}
// TODO: fn _measures should record the marks in a lookup
pub fn _clearMeasures(_: *Performance, name: ?[]const u8) void {
_ = name;
}
};
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry
@@ -137,23 +132,17 @@ pub const PerformanceMark = struct {
proto: PerformanceEntry,
detail: ?Env.JsObject,
const Options = struct {
detail: ?Env.JsObject = null,
start_time: ?f64 = null,
};
pub fn constructor(name: []const u8, _options: ?Options, page: *Page) !PerformanceMark {
pub fn constructor(name: []const u8, _options: ?MarkOptions, page: *Page) !PerformanceMark {
const perf = &page.window.performance;
const options = _options orelse Options{};
const options = _options orelse MarkOptions{};
const start_time = options.start_time orelse perf._now();
const detail = if (options.detail) |d| try d.persist() else null;
if (start_time < 0.0) {
return error.TypeError;
}
const detail = if (options.detail) |d| try d.persist() else null;
const duped_name = try page.arena.dupe(u8, name);
const proto = PerformanceEntry{ .name = duped_name, .entry_type = .mark, .start_time = start_time };

View File

@@ -17,39 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Env = @import("../env.zig").Env;
const PerformanceEntry = @import("performance.zig").PerformanceEntry;
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver
pub const PerformanceObserver = struct {
pub const _supportedEntryTypes = [0][]const u8{};
pub fn constructor(cbk: Env.Function) PerformanceObserver {
_ = cbk;
return .{};
}
pub fn _observe(self: *const PerformanceObserver, options_: ?Options) void {
_ = self;
_ = options_;
return;
}
pub fn _disconnect(self: *PerformanceObserver) void {
_ = self;
}
pub fn _takeRecords(_: *const PerformanceObserver) []PerformanceEntry {
return &[_]PerformanceEntry{};
}
};
const Options = struct {
buffered: ?bool = null,
durationThreshold: ?f64 = null,
entryTypes: ?[]const []const u8 = null,
type: ?[]const u8 = null,
};
const testing = @import("../../testing.zig");

View File

@@ -1,73 +0,0 @@
// 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 parser = @import("../netsurf.zig");
const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union;
// WEB IDL https://dom.spec.whatwg.org/#interface-shadowroot
pub const ShadowRoot = struct {
pub const prototype = *parser.DocumentFragment;
pub const subtype = .node;
mode: Mode,
host: *parser.Element,
proto: *parser.DocumentFragment,
pub const Mode = enum {
open,
closed,
};
pub fn get_host(self: *const ShadowRoot) !ElementUnion {
return Element.toInterface(self.host);
}
};
const testing = @import("../../testing.zig");
test "Browser.DOM.ShadowRoot" {
defer testing.reset();
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
defer runner.deinit();
try runner.testCases(&.{
.{ "const div1 = document.createElement('div');", null },
.{ "let sr1 = div1.attachShadow({mode: 'open'})", null },
.{ "sr1.host == div1", "true" },
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
.{ "div1.shadowRoot == sr1", "true" },
.{ "try { div1.attachShadow({mode: 'closed'}) } catch (e) { e }", "Error: NotSupportedError" },
.{ " sr1.append(document.createElement('div'))", null },
.{ " sr1.append(document.createElement('span'))", null },
.{ "sr1.childElementCount", "2" },
// re-attaching clears it
.{ "div1.attachShadow({mode: 'open'}) == sr1", "true" },
.{ "sr1.childElementCount", "0" },
}, .{});
try runner.testCases(&.{
.{ "const div2 = document.createElement('di2');", null },
.{ "let sr2 = div2.attachShadow({mode: 'closed'})", null },
.{ "sr2.host == div2", "true" },
.{ "div2.shadowRoot", "null" }, // null when attached with 'closed'
}, .{});
}

View File

@@ -19,19 +19,16 @@
const std = @import("std");
const parser = @import("../netsurf.zig");
const NodeFilter = @import("node_filter.zig");
const NodeFilter = @import("node_filter.zig").NodeFilter;
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const Node = @import("node.zig").Node;
const NodeUnion = @import("node.zig").Union;
// https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
pub const TreeWalker = struct {
root: *parser.Node,
current_node: *parser.Node,
what_to_show: u32,
filter: ?TreeWalkerOpts,
filter_func: ?Env.Function,
filter: ?Env.Function,
pub const TreeWalkerOpts = union(enum) {
function: Env.Function,
@@ -51,25 +48,58 @@ pub const TreeWalker = struct {
return .{
.root = node,
.current_node = node,
.what_to_show = what_to_show orelse NodeFilter.NodeFilter._SHOW_ALL,
.filter = filter,
.filter_func = filter_func,
.what_to_show = what_to_show orelse NodeFilter._SHOW_ALL,
.filter = filter_func,
};
}
pub fn get_root(self: *TreeWalker) !NodeUnion {
return try Node.toInterface(self.root);
const VerifyResult = enum { accept, skip, reject };
fn verify(self: *const TreeWalker, node: *parser.Node) !VerifyResult {
const node_type = try parser.nodeType(node);
const what_to_show = self.what_to_show;
// Verify that we can show this node type.
if (!switch (node_type) {
.attribute => what_to_show & NodeFilter._SHOW_ATTRIBUTE != 0,
.cdata_section => what_to_show & NodeFilter._SHOW_CDATA_SECTION != 0,
.comment => what_to_show & NodeFilter._SHOW_COMMENT != 0,
.document => what_to_show & NodeFilter._SHOW_DOCUMENT != 0,
.document_fragment => what_to_show & NodeFilter._SHOW_DOCUMENT_FRAGMENT != 0,
.document_type => what_to_show & NodeFilter._SHOW_DOCUMENT_TYPE != 0,
.element => what_to_show & NodeFilter._SHOW_ELEMENT != 0,
.entity => what_to_show & NodeFilter._SHOW_ENTITY != 0,
.entity_reference => what_to_show & NodeFilter._SHOW_ENTITY_REFERENCE != 0,
.notation => what_to_show & NodeFilter._SHOW_NOTATION != 0,
.processing_instruction => what_to_show & NodeFilter._SHOW_PROCESSING_INSTRUCTION != 0,
.text => what_to_show & NodeFilter._SHOW_TEXT != 0,
}) return .reject;
// Verify that we aren't filtering it out.
if (self.filter) |f| {
const filter = try f.call(u32, .{node});
return switch (filter) {
NodeFilter._FILTER_ACCEPT => .accept,
NodeFilter._FILTER_REJECT => .reject,
NodeFilter._FILTER_SKIP => .skip,
else => .reject,
};
} else return .accept;
}
pub fn get_currentNode(self: *TreeWalker) !NodeUnion {
return try Node.toInterface(self.current_node);
pub fn get_root(self: *TreeWalker) *parser.Node {
return self.root;
}
pub fn get_currentNode(self: *TreeWalker) *parser.Node {
return self.current_node;
}
pub fn get_whatToShow(self: *TreeWalker) u32 {
return self.what_to_show;
}
pub fn get_filter(self: *TreeWalker) ?TreeWalkerOpts {
pub fn get_filter(self: *TreeWalker) ?Env.Function {
return self.filter;
}
@@ -85,7 +115,7 @@ pub const TreeWalker = struct {
const index: u32 = @intCast(i);
const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
switch (try self.verify(child)) {
.accept => return child,
.reject => continue,
.skip => if (try self.firstChild(child)) |gchild| return gchild,
@@ -104,7 +134,7 @@ pub const TreeWalker = struct {
index -= 1;
const child = (try parser.nodeListItem(children, index)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, child)) {
switch (try self.verify(child)) {
.accept => return child,
.reject => continue,
.skip => if (try self.lastChild(child)) |gchild| return gchild,
@@ -120,7 +150,7 @@ pub const TreeWalker = struct {
while (true) {
current = (try parser.nodeNextSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
switch (try self.verify(current)) {
.accept => return current,
.skip, .reject => continue,
}
@@ -135,7 +165,7 @@ pub const TreeWalker = struct {
while (true) {
current = (try parser.nodePreviousSibling(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
switch (try self.verify(current)) {
.accept => return current,
.skip, .reject => continue,
}
@@ -152,42 +182,42 @@ pub const TreeWalker = struct {
if (current == self.root) return null;
current = (try parser.nodeParentNode(current)) orelse return null;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
switch (try self.verify(current)) {
.accept => return current,
.reject, .skip => continue,
}
}
}
pub fn _firstChild(self: *TreeWalker) !?NodeUnion {
pub fn _firstChild(self: *TreeWalker) !?*parser.Node {
if (try self.firstChild(self.current_node)) |child| {
self.current_node = child;
return try Node.toInterface(child);
return child;
}
return null;
}
pub fn _lastChild(self: *TreeWalker) !?NodeUnion {
pub fn _lastChild(self: *TreeWalker) !?*parser.Node {
if (try self.lastChild(self.current_node)) |child| {
self.current_node = child;
return try Node.toInterface(child);
return child;
}
return null;
}
pub fn _nextNode(self: *TreeWalker) !?NodeUnion {
pub fn _nextNode(self: *TreeWalker) !?*parser.Node {
if (try self.firstChild(self.current_node)) |child| {
self.current_node = child;
return try Node.toInterface(child);
return child;
}
var current = self.current_node;
while (current != self.root) {
if (try self.nextSibling(current)) |sibling| {
self.current_node = sibling;
return try Node.toInterface(sibling);
return sibling;
}
current = (try parser.nodeParentNode(current)) orelse break;
@@ -196,49 +226,47 @@ pub const TreeWalker = struct {
return null;
}
pub fn _nextSibling(self: *TreeWalker) !?NodeUnion {
pub fn _nextSibling(self: *TreeWalker) !?*parser.Node {
if (try self.nextSibling(self.current_node)) |sibling| {
self.current_node = sibling;
return try Node.toInterface(sibling);
return sibling;
}
return null;
}
pub fn _parentNode(self: *TreeWalker) !?NodeUnion {
pub fn _parentNode(self: *TreeWalker) !?*parser.Node {
if (try self.parentNode(self.current_node)) |parent| {
self.current_node = parent;
return try Node.toInterface(parent);
return parent;
}
return null;
}
pub fn _previousNode(self: *TreeWalker) !?NodeUnion {
if (self.current_node == self.root) return null;
pub fn _previousNode(self: *TreeWalker) !?*parser.Node {
var current = self.current_node;
while (try parser.nodePreviousSibling(current)) |previous| {
current = previous;
switch (try NodeFilter.verify(self.what_to_show, self.filter_func, current)) {
switch (try self.verify(current)) {
.accept => {
// Get last child if it has one.
if (try self.lastChild(current)) |child| {
self.current_node = child;
return try Node.toInterface(child);
return child;
}
// Otherwise, this node is our previous one.
self.current_node = current;
return try Node.toInterface(current);
return current;
},
.reject => continue,
.skip => {
// Get last child if it has one.
if (try self.lastChild(current)) |child| {
self.current_node = child;
return try Node.toInterface(child);
return child;
}
},
}
@@ -247,17 +275,17 @@ pub const TreeWalker = struct {
if (current != self.root) {
if (try self.parentNode(current)) |parent| {
self.current_node = parent;
return try Node.toInterface(parent);
return parent;
}
}
return null;
}
pub fn _previousSibling(self: *TreeWalker) !?NodeUnion {
pub fn _previousSibling(self: *TreeWalker) !?*parser.Node {
if (try self.previousSibling(self.current_node)) |sibling| {
self.current_node = sibling;
return try Node.toInterface(sibling);
return sibling;
}
return null;

View File

@@ -21,14 +21,10 @@ const std = @import("std");
const parser = @import("netsurf.zig");
const Walker = @import("dom/walker.zig").WalkerChildren;
pub const Opts = struct {
exclude_scripts: bool = false,
};
// writer must be a std.io.Writer
pub fn writeHTML(doc: *parser.Document, opts: Opts, writer: anytype) !void {
pub fn writeHTML(doc: *parser.Document, writer: anytype) !void {
try writer.writeAll("<!DOCTYPE html>\n");
try writeChildren(parser.documentToNode(doc), opts, writer);
try writeChildren(parser.documentToNode(doc), writer);
try writer.writeAll("\n");
}
@@ -58,15 +54,10 @@ pub fn writeDocType(doc_type: *parser.DocumentType, writer: anytype) !void {
try writer.writeAll(">");
}
pub fn writeNode(node: *parser.Node, opts: Opts, writer: anytype) anyerror!void {
pub fn writeNode(node: *parser.Node, writer: anytype) anyerror!void {
switch (try parser.nodeType(node)) {
.element => {
// open the tag
const tag_type = try parser.elementHTMLGetTagType(@ptrCast(node));
if (tag_type == .script and opts.exclude_scripts) {
return;
}
const tag = try parser.nodeLocalName(node);
try writer.writeAll("<");
try writer.writeAll(tag);
@@ -91,12 +82,12 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: anytype) anyerror!void
// void elements can't have any content.
if (try isVoid(parser.nodeToElement(node))) return;
if (tag_type == .script) {
if (try parser.elementHTMLGetTagType(@ptrCast(node)) == .script) {
try writer.writeAll(try parser.nodeTextContent(node) orelse "");
} else {
// write the children
// TODO avoid recursion
try writeChildren(node, opts, writer);
try writeChildren(node, writer);
}
// close the tag
@@ -138,12 +129,12 @@ pub fn writeNode(node: *parser.Node, opts: Opts, writer: anytype) anyerror!void
}
// writer must be a std.io.Writer
pub fn writeChildren(root: *parser.Node, opts: Opts, writer: anytype) !void {
pub fn writeChildren(root: *parser.Node, writer: anytype) !void {
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(root, next) orelse break;
try writeNode(next.?, opts, writer);
try writeNode(next.?, writer);
}
}
@@ -247,6 +238,6 @@ fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
defer parser.documentHTMLClose(doc_html) catch {};
const doc = parser.documentHTMLToDocument(doc_html);
try writeHTML(doc, .{}, buf.writer(testing.allocator));
try writeHTML(doc, buf.writer(testing.allocator));
try testing.expectEqualStrings(expected, buf.items);
}

View File

@@ -1,107 +0,0 @@
// 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 Env = @import("../env.zig").Env;
// https://encoding.spec.whatwg.org/#interface-textdecoder
const TextDecoder = @This();
const SupportedLabels = enum {
utf8,
@"utf-8",
@"unicode-1-1-utf-8",
};
const Options = struct {
fatal: bool = false,
ignoreBOM: bool = false,
};
fatal: bool,
ignore_bom: bool,
pub fn constructor(label_: ?[]const u8, opts_: ?Options) !TextDecoder {
if (label_) |l| {
_ = std.meta.stringToEnum(SupportedLabels, l) orelse {
log.warn(.web_api, "not implemented", .{ .feature = "TextDecoder label", .label = l });
return error.NotImplemented;
};
}
const opts = opts_ orelse Options{};
return .{
.fatal = opts.fatal,
.ignore_bom = opts.ignoreBOM,
};
}
pub fn get_encoding(_: *const TextDecoder) []const u8 {
return "utf-8";
}
pub fn get_ignoreBOM(self: *const TextDecoder) bool {
return self.ignore_bom;
}
pub fn get_fatal(self: *const TextDecoder) bool {
return self.fatal;
}
// TODO: Should accept an ArrayBuffer, TypedArray or DataView
// js.zig will currently only map a TypedArray to our []const u8.
pub fn _decode(self: *const TextDecoder, v: []const u8) ![]const u8 {
if (self.fatal and !std.unicode.utf8ValidateSlice(v)) {
return error.InvalidUtf8;
}
if (self.ignore_bom == false and std.mem.startsWith(u8, v, &.{ 0xEF, 0xBB, 0xBF })) {
return v[3..];
}
return v;
}
const testing = @import("../../testing.zig");
test "Browser.Encoding.TextDecoder" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{
.html = "",
});
defer runner.deinit();
try runner.testCases(&.{
.{ "let d1 = new TextDecoder();", null },
.{ "d1.encoding;", "utf-8" },
.{ "d1.fatal", "false" },
.{ "d1.ignoreBOM", "false" },
.{ "d1.decode(new Uint8Array([240, 160, 174, 183]))", "𠮷" },
.{ "d1.decode(new Uint8Array([0xEF, 0xBB, 0xBF, 240, 160, 174, 183]))", "𠮷" },
.{ "d1.decode(new Uint8Array([49, 50]).buffer)", "12" },
.{ "let d2 = new TextDecoder('utf8', {fatal: true})", null },
.{
\\ try {
\\ let data = new Uint8Array([240, 240, 160, 174, 183]);
\\ d2.decode(data);
\\ } catch (e) {e}
,
"Error: InvalidUtf8",
},
}, .{});
}

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -20,37 +20,39 @@ const std = @import("std");
const Env = @import("../env.zig").Env;
pub const Interfaces = .{
TextEncoder,
};
// https://encoding.spec.whatwg.org/#interface-textencoder
const TextEncoder = @This();
pub fn constructor() !TextEncoder {
return .{};
}
pub fn get_encoding(_: *const TextEncoder) []const u8 {
return "utf-8";
}
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
// Ensure the input is a valid utf-8
// It seems chrome accepts invalid utf-8 sequence.
//
if (!std.unicode.utf8ValidateSlice(v)) {
return error.InvalidUtf8;
pub const TextEncoder = struct {
pub fn constructor() !TextEncoder {
return .{};
}
return .{ .values = v };
}
pub fn get_encoding(_: *const TextEncoder) []const u8 {
return "utf-8";
}
pub fn _encode(_: *const TextEncoder, v: []const u8) !Env.TypedArray(u8) {
// Ensure the input is a valid utf-8
// It seems chrome accepts invalid utf-8 sequence.
//
if (!std.unicode.utf8ValidateSlice(v)) {
return error.InvalidUtf8;
}
return .{ .values = v };
}
};
const testing = @import("../../testing.zig");
test "Browser.Encoding.TextEncoder" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{
.html = "",
});
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "var encoder = new TextEncoder();", null },
.{ "var encoder = new TextEncoder();", "undefined" },
.{ "encoder.encoding;", "utf-8" },
.{ "encoder.encode('€');", "226,130,172" },

View File

@@ -25,8 +25,7 @@ const WebApis = struct {
@import("css/css.zig").Interfaces,
@import("cssom/cssom.zig").Interfaces,
@import("dom/dom.zig").Interfaces,
@import("dom/shadow_root.zig").ShadowRoot,
@import("encoding/encoding.zig").Interfaces,
@import("encoding/text_encoder.zig").Interfaces,
@import("events/event.zig").Interfaces,
@import("html/html.zig").Interfaces,
@import("iterator/iterator.zig").Interfaces,
@@ -35,14 +34,12 @@ const WebApis = struct {
@import("xhr/xhr.zig").Interfaces,
@import("xhr/form_data.zig").Interfaces,
@import("xmlserializer/xmlserializer.zig").Interfaces,
@import("webcomponents/webcomponents.zig").Interfaces,
});
};
pub const JsThis = Env.JsThis;
pub const JsObject = Env.JsObject;
pub const Function = Env.Function;
pub const Promise = Env.Promise;
pub const PromiseResolver = Env.PromiseResolver;
pub const Env = js.Env(*Page, WebApis);
pub const Global = @import("html/window.zig").Window;

View File

@@ -33,10 +33,9 @@ const CustomEvent = @import("custom_event.zig").CustomEvent;
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
const MouseEvent = @import("mouse_event.zig").MouseEvent;
const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
// Event interfaces
pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent };
pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent };
pub const Union = generate.Union(Interfaces);
@@ -61,7 +60,6 @@ pub const Event = struct {
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
.mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) },
.error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* },
.message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* },
};
}
@@ -80,13 +78,13 @@ pub const Event = struct {
pub fn get_target(self: *parser.Event, page: *Page) !?EventTargetUnion {
const et = try parser.eventTarget(self);
if (et == null) return null;
return try EventTarget.toInterface(et.?, page);
return try EventTarget.toInterface(self, et.?, page);
}
pub fn get_currentTarget(self: *parser.Event, page: *Page) !?EventTargetUnion {
const et = try parser.eventCurrentTarget(self);
if (et == null) return null;
return try EventTarget.toInterface(et.?, page);
return try EventTarget.toInterface(self, et.?, page);
}
pub fn get_eventPhase(self: *parser.Event) !u8 {

View File

@@ -56,12 +56,13 @@ pub const AbortSignal = struct {
const DEFAULT_REASON = "AbortError";
pub const prototype = *EventTarget;
proto: parser.EventTargetTBase = .{ .internal_target_type = .abort_signal },
proto: parser.EventTargetTBase = .{},
aborted: bool,
reason: ?[]const u8,
pub const init: AbortSignal = .{
.proto = .{},
.reason = null,
.aborted = false,
};

View File

@@ -42,12 +42,8 @@ pub const HTMLDocument = struct {
// JS funcs
// --------
pub fn get_domain(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
// libdom's document_html get_domain always returns null, this is
// the way MDN recommends getting the domain anyways, since document.domain
// is deprecated.
const location = try parser.documentHTMLGetLocation(Location, self) orelse return "";
return location.get_host(page);
pub fn get_domain(self: *parser.DocumentHTML) ![]const u8 {
return try parser.documentHTMLGetDomain(self);
}
pub fn set_domain(_: *parser.DocumentHTML, _: []const u8) ![]const u8 {
@@ -278,17 +274,15 @@ pub const HTMLDocument = struct {
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
state.ready_state = .interactive;
const evt = try parser.eventCreate();
defer parser.eventDestroy(evt);
log.debug(.script_event, "dispatch event", .{
.type = "DOMContentLoaded",
.source = "document",
});
const evt = try parser.eventCreate();
defer parser.eventDestroy(evt);
try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true });
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, self), evt);
try page.window.dispatchForDocumentTarget(evt);
}
pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void {
@@ -313,7 +307,7 @@ test "Browser.HTML.Document" {
}, .{});
try runner.testCases(&.{
.{ "document.domain", "lightpanda.io" },
.{ "document.domain", "" },
.{ "document.referrer", "" },
.{ "document.title", "" },
.{ "document.body.localName", "body" },

View File

@@ -29,7 +29,6 @@ const Node = @import("../dom/node.zig").Node;
const Element = @import("../dom/element.zig").Element;
const DataSet = @import("DataSet.zig");
const StyleSheet = @import("../cssom/stylesheet.zig").StyleSheet;
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
// HTMLElement interfaces
@@ -115,6 +114,10 @@ pub const HTMLElement = struct {
pub const prototype = *Element;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
pub fn get_style(e: *parser.ElementHTML, page: *Page) !*CSSStyleDeclaration {
const state = try page.getOrCreateNodeState(@ptrCast(e));
return &state.style;
@@ -186,6 +189,10 @@ pub const HTMLMediaElement = struct {
pub const Self = parser.MediaElement;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
// HTML elements
@@ -195,6 +202,10 @@ pub const HTMLUnknownElement = struct {
pub const Self = parser.Unknown;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
// https://html.spec.whatwg.org/#the-a-element
@@ -203,6 +214,10 @@ pub const HTMLAnchorElement = struct {
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
pub fn get_target(self: *parser.Anchor) ![]const u8 {
return try parser.anchorGetTarget(self);
}
@@ -450,144 +465,240 @@ pub const HTMLAppletElement = struct {
pub const Self = parser.Applet;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLAreaElement = struct {
pub const Self = parser.Area;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLAudioElement = struct {
pub const Self = parser.Audio;
pub const prototype = *HTMLMediaElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLBRElement = struct {
pub const Self = parser.BR;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLBaseElement = struct {
pub const Self = parser.Base;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLBodyElement = struct {
pub const Self = parser.Body;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLButtonElement = struct {
pub const Self = parser.Button;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLCanvasElement = struct {
pub const Self = parser.Canvas;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLDListElement = struct {
pub const Self = parser.DList;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLDataElement = struct {
pub const Self = parser.Data;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLDataListElement = struct {
pub const Self = parser.DataList;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLDialogElement = struct {
pub const Self = parser.Dialog;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLDirectoryElement = struct {
pub const Self = parser.Directory;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLDivElement = struct {
pub const Self = parser.Div;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLEmbedElement = struct {
pub const Self = parser.Embed;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLFieldSetElement = struct {
pub const Self = parser.FieldSet;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLFontElement = struct {
pub const Self = parser.Font;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLFrameElement = struct {
pub const Self = parser.Frame;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLFrameSetElement = struct {
pub const Self = parser.FrameSet;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLHRElement = struct {
pub const Self = parser.HR;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLHeadElement = struct {
pub const Self = parser.Head;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLHeadingElement = struct {
pub const Self = parser.Heading;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLHtmlElement = struct {
pub const Self = parser.Html;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLIFrameElement = struct {
pub const Self = parser.IFrame;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLImageElement = struct {
@@ -595,6 +706,10 @@ pub const HTMLImageElement = struct {
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
pub fn get_alt(self: *parser.Image) ![]const u8 {
return try parser.imageGetAlt(self);
}
@@ -654,6 +769,10 @@ pub const HTMLInputElement = struct {
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
pub fn get_defaultValue(self: *parser.Input) ![]const u8 {
return try parser.inputGetDefaultValue(self);
}
@@ -742,18 +861,30 @@ pub const HTMLLIElement = struct {
pub const Self = parser.LI;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLLabelElement = struct {
pub const Self = parser.Label;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLLegendElement = struct {
pub const Self = parser.Legend;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLLinkElement = struct {
@@ -761,6 +892,10 @@ pub const HTMLLinkElement = struct {
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
pub fn get_href(self: *parser.Link) ![]const u8 {
return try parser.linkGetHref(self);
}
@@ -775,90 +910,150 @@ pub const HTMLMapElement = struct {
pub const Self = parser.Map;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLMetaElement = struct {
pub const Self = parser.Meta;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLMeterElement = struct {
pub const Self = parser.Meter;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLModElement = struct {
pub const Self = parser.Mod;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLOListElement = struct {
pub const Self = parser.OList;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLObjectElement = struct {
pub const Self = parser.Object;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLOptGroupElement = struct {
pub const Self = parser.OptGroup;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLOptionElement = struct {
pub const Self = parser.Option;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLOutputElement = struct {
pub const Self = parser.Output;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLParagraphElement = struct {
pub const Self = parser.Paragraph;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLParamElement = struct {
pub const Self = parser.Param;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLPictureElement = struct {
pub const Self = parser.Picture;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLPreElement = struct {
pub const Self = parser.Pre;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLProgressElement = struct {
pub const Self = parser.Progress;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLQuoteElement = struct {
pub const Self = parser.Quote;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
// https://html.spec.whatwg.org/#the-script-element
@@ -867,6 +1062,10 @@ pub const HTMLScriptElement = struct {
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
pub fn get_src(self: *parser.Script) !?[]const u8 {
return try parser.elementGetAttribute(
parser.scriptToElt(self),
@@ -1001,12 +1200,20 @@ pub const HTMLSourceElement = struct {
pub const Self = parser.Source;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLSpanElement = struct {
pub const Self = parser.Span;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLStyleElement = struct {
@@ -1014,16 +1221,8 @@ pub const HTMLStyleElement = struct {
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn get_sheet(self: *parser.Style, page: *Page) !*StyleSheet {
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
if (state.style_sheet) |ss| {
return ss;
}
const ss = try page.arena.create(StyleSheet);
ss.* = .{};
state.style_sheet = ss;
return ss;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
@@ -1031,36 +1230,60 @@ pub const HTMLTableElement = struct {
pub const Self = parser.Table;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLTableCaptionElement = struct {
pub const Self = parser.TableCaption;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLTableCellElement = struct {
pub const Self = parser.TableCell;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLTableColElement = struct {
pub const Self = parser.TableCol;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLTableRowElement = struct {
pub const Self = parser.TableRow;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLTableSectionElement = struct {
pub const Self = parser.TableSection;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLTemplateElement = struct {
@@ -1068,6 +1291,10 @@ pub const HTMLTemplateElement = struct {
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
pub fn get_content(self: *parser.Template, page: *Page) !*parser.DocumentFragment {
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
if (state.template_content) |tc| {
@@ -1083,36 +1310,60 @@ pub const HTMLTextAreaElement = struct {
pub const Self = parser.TextArea;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLTimeElement = struct {
pub const Self = parser.Time;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLTitleElement = struct {
pub const Self = parser.Title;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLTrackElement = struct {
pub const Self = parser.Track;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLUListElement = struct {
pub const Self = parser.UList;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub const HTMLVideoElement = struct {
pub const Self = parser.Video;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
}
};
pub fn toInterface(comptime T: type, e: *parser.Element) !T {
@@ -1190,6 +1441,16 @@ pub fn toInterface(comptime T: type, e: *parser.Element) !T {
};
}
fn constructHtmlElement(page: *Page, js_this: Env.JsThis) !*parser.Element {
const constructor_name = try js_this.constructorName(page.call_arena);
if (!page.window.custom_elements.lookup.contains(constructor_name)) {
return error.IllegalContructor;
}
const el = try parser.documentCreateElement(@ptrCast(page.window.document), constructor_name);
return el;
}
const testing = @import("../../testing.zig");
test "Browser.HTML.Element" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
@@ -1466,18 +1727,6 @@ test "Browser.HTML.HTMLTemplateElement" {
}, .{});
}
test "Browser.HTML.HTMLStyleElement" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "" });
defer runner.deinit();
try runner.testCases(&.{
.{ "let s = document.createElement('style')", null },
.{ "s.sheet.type", "text/css" },
.{ "s.sheet == s.sheet", "true" },
.{ "document.createElement('style').sheet == s.sheet", "false" },
}, .{});
}
const Check = struct {
input: []const u8,
expected: ?[]const u8 = null, // Needed when input != expected

View File

@@ -26,7 +26,7 @@ pub const MediaQueryList = struct {
// Extend libdom event target for pure zig struct.
// This is not safe as it relies on a structure layout that isn't guaranteed
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .media_query_list },
base: parser.EventTargetTBase = parser.EventTargetTBase{},
matches: bool,
media: []const u8,

View File

@@ -20,7 +20,7 @@ const std = @import("std");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Function = @import("../env.zig").Function;
const Page = @import("../page.zig").Page;
const Loop = @import("../../runtime/loop.zig").Loop;
@@ -33,12 +33,10 @@ const EventTarget = @import("../dom/event_target.zig").EventTarget;
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
const Performance = @import("../dom/performance.zig").Performance;
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
const CustomElementRegistry = @import("../webcomponents/custom_element_registry.zig").CustomElementRegistry;
const Screen = @import("screen.zig").Screen;
const Css = @import("../css/css.zig").Css;
const Function = Env.Function;
const JsObject = Env.JsObject;
const storage = @import("../storage/storage.zig");
// https://dom.spec.whatwg.org/#interface-window-extensions
@@ -47,7 +45,7 @@ pub const Window = struct {
pub const prototype = *EventTarget;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .window },
base: parser.EventTargetTBase = parser.EventTargetTBase{},
document: *parser.DocumentHTML,
target: []const u8 = "",
@@ -63,6 +61,7 @@ pub const Window = struct {
console: Console = .{},
navigator: Navigator = .{},
performance: Performance,
custom_elements: CustomElementRegistry = .{},
screen: Screen = .{},
css: Css = .{},
@@ -170,6 +169,10 @@ pub const Window = struct {
return &self.performance;
}
pub fn get_customElements(self: *Window) *CustomElementRegistry {
return &self.custom_elements;
}
pub fn get_screen(self: *Window) *Screen {
return &self.screen;
}
@@ -187,12 +190,14 @@ pub const Window = struct {
return page.loop.cancel(kv.value.loop_id);
}
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .args = params });
// TODO handle callback arguments.
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{});
}
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params });
// TODO handle callback arguments.
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .repeat = true });
}
pub fn _clearTimeout(self: *Window, id: u32, page: *Page) !void {
@@ -205,10 +210,6 @@ pub const Window = struct {
return page.loop.cancel(kv.value.loop_id);
}
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
return self.createTimeout(cbk, 0, page, .{});
}
pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
return .{
.matches = false, // TODO?
@@ -232,11 +233,10 @@ pub const Window = struct {
}
const CreateTimeoutOpts = struct {
args: []Env.JsObject = &.{},
repeat: bool = false,
animation_frame: bool = false,
};
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, opts: CreateTimeoutOpts) !u32 {
fn createTimeout(self: *Window, cbk: Function, delay_: ?u32, page: *Page, comptime opts: CreateTimeoutOpts) !u32 {
const delay = delay_ orelse 0;
if (delay > 5000) {
log.warn(.user_script, "long timeout ignored", .{ .delay = delay, .interval = opts.repeat });
@@ -261,15 +261,6 @@ pub const Window = struct {
}
errdefer _ = self.timers.remove(timer_id);
const args = opts.args;
var persisted_args: []Env.JsObject = &.{};
if (args.len > 0) {
persisted_args = try page.arena.alloc(Env.JsObject, args.len);
for (args, persisted_args) |a, *ca| {
ca.* = try a.persist();
}
}
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
const callback = try arena.create(TimerCallback);
@@ -278,7 +269,6 @@ pub const Window = struct {
.loop_id = 0, // we're going to set this to a real value shortly
.window = self,
.timer_id = timer_id,
.args = persisted_args,
.node = .{ .func = TimerCallback.run },
.repeat = if (opts.repeat) delay_ms else null,
.animation_frame = opts.animation_frame,
@@ -334,23 +324,6 @@ pub const Window = struct {
);
}
}
// libdom's document doesn't have a parent, which is correct, but
// breaks the event bubbling that happens for many events from
// document -> window.
// We need to force dispatch this event on the window, with the
// document target.
// In theory, we should do this for a lot of events and might need
// to come up with a good way to solve this more generically. But
// this specific event, and maybe a few others in the near future,
// are blockers.
// Worth noting that NetSurf itself appears to do something similar:
// https://github.com/netsurf-browser/netsurf/blob/a32e1a03e1c91ee9f0aa211937dbae7a96831149/content/handlers/html/html.c#L380
pub fn dispatchForDocumentTarget(self: *Window, evt: *parser.Event) !void {
// we assume that this evt has already been dispatched on the document
// and thus the target has already been set to the document.
return self.base.redispatchEvent(evt);
}
};
const TimerCallback = struct {
@@ -374,8 +347,6 @@ const TimerCallback = struct {
window: *Window,
args: []Env.JsObject = &.{},
fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
const self: *TimerCallback = @fieldParentPtr("node", node);
@@ -385,11 +356,11 @@ const TimerCallback = struct {
if (self.animation_frame) {
call = self.cbk.tryCall(void, .{self.window.performance._now()}, &result);
} else {
call = self.cbk.tryCall(void, self.args, &result);
call = self.cbk.tryCall(void, .{}, &result);
}
call catch {
log.warn(.user_script, "callback error", .{
log.debug(.user_script, "callback error", .{
.err = result.exception,
.stack = result.stack,
.source = "window timeout",
@@ -459,17 +430,11 @@ test "Browser.HTML.Window" {
.{ "innerWidth", "2" },
}, .{});
// cancelAnimationFrame should be able to cancel a request with the given id
try runner.testCases(&.{
.{ "let longCall = false;", null },
.{ "window.setTimeout(() => {longCall = true}, 5001);", null },
.{ "longCall;", "false" },
.{ "let wst = 0;", null },
.{ "window.setTimeout(() => {wst += 1}, 1)", null },
.{ "wst", "1" },
.{ "window.setTimeout((a, b) => {wst = a + b}, 1, 2, 3)", null },
.{ "wst", "5" },
}, .{});
// window event target
@@ -503,26 +468,4 @@ test "Browser.HTML.Window" {
.{ "scroll", "true" },
.{ "scrollend", "true" },
}, .{});
try runner.testCases(&.{
.{ "var qm = false; window.queueMicrotask(() => {qm = true });", null },
.{ "qm", "true" },
}, .{});
{
try runner.testCases(&.{
.{
\\ let dcl = false;
\\ window.addEventListener('DOMContentLoaded', (e) => {
\\ dcl = e.target == document;
\\ });
,
null,
},
}, .{});
try runner.dispatchDOMContentLoaded();
try runner.testCases(&.{
.{ "dcl", "true" },
}, .{});
}
}

View File

@@ -528,7 +528,6 @@ pub const EventType = enum(u8) {
error_event = 4,
abort_signal = 5,
xhr_event = 6,
message_event = 7,
};
pub const MutationEvent = c.dom_mutation_event;
@@ -750,13 +749,6 @@ pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool {
return res;
}
pub fn eventTargetInternalType(et: *EventTarget) !EventTargetTBase.InternalType {
var res: u32 = undefined;
const err = eventTargetVtable(et).internal_type.?(et, &res);
try DOMErr(err);
return @enumFromInt(res);
}
pub fn elementDispatchEvent(element: *Element, event: *Event) !bool {
const et: *EventTarget = toEventTarget(Element, element);
return eventTargetDispatchEvent(et, @ptrCast(event));
@@ -779,23 +771,12 @@ pub fn eventTargetTBaseFieldName(comptime T: type) ?[]const u8 {
// EventTargetBase is used to implement EventTarget for pure zig struct.
pub const EventTargetTBase = extern struct {
const Self = @This();
const InternalType = enum(u32) {
libdom_node = 0,
plain = 1,
abort_signal = 2,
xhr = 3,
window = 4,
performance = 5,
media_query_list = 6,
message_port = 7,
};
vtable: ?*const c.struct_dom_event_target_vtable = &c.struct_dom_event_target_vtable{
.dispatch_event = dispatch_event,
.remove_event_listener = remove_event_listener,
.add_event_listener = add_event_listener,
.iter_event_listener = iter_event_listener,
.internal_type = internal_type,
},
// When we dispatch the event, we need to provide a target. In reality, the
@@ -809,7 +790,6 @@ pub const EventTargetTBase = extern struct {
refcnt: u32 = 0,
eti: c.dom_event_target_internal = c.dom_event_target_internal{ .listeners = null },
internal_target_type: InternalType,
pub fn add_event_listener(et: [*c]c.dom_event_target, t: [*c]c.dom_string, l: ?*c.struct_dom_event_listener, capture: bool) callconv(.C) c.dom_exception {
const self = @as(*Self, @ptrCast(et));
@@ -842,20 +822,6 @@ pub const EventTargetTBase = extern struct {
const self = @as(*Self, @ptrCast(et));
return c._dom_event_target_iter_event_listener(self.eti, t, capture, cur, next, l);
}
pub fn internal_type(et: [*c]c.dom_event_target, internal_type_: [*c]u32) callconv(.C) c.dom_exception {
const self = @as(*Self, @ptrCast(et));
internal_type_.* = @intFromEnum(self.internal_target_type);
return c.DOM_NO_ERR;
}
// Called to simulate bubbling from a libdom node (e.g. the Document) to a
// Zig instance (e.g. the Window).
pub fn redispatchEvent(self: *EventTargetTBase, evt: *Event) !void {
var res: bool = undefined;
const err = c._dom_event_target_dispatch(@ptrCast(self), &self.eti, evt, c.DOM_BUBBLING_PHASE, &res);
try DOMErr(err);
}
};
// MouseEvent
@@ -1959,6 +1925,18 @@ pub inline fn documentFragmentToNode(doc: *DocumentFragment) *Node {
return @as(*Node, @alignCast(@ptrCast(doc)));
}
pub fn documentFragmentBodyChildren(doc: *DocumentFragment) !?*NodeList {
const node = documentFragmentToNode(doc);
const html = try nodeFirstChild(node) orelse return null;
// TODO unref
const head = try nodeFirstChild(html) orelse return null;
// TODO unref
const body = try nodeNextSibling(head) orelse return null;
// TODO unref
return try nodeGetChildNodes(body);
}
// Document Position
pub const DocumentPosition = enum(u32) {
@@ -2406,6 +2384,14 @@ pub inline fn documentHTMLSetBody(doc_html: *DocumentHTML, elt: ?*ElementHTML) !
try DOMErr(err);
}
pub inline fn documentHTMLGetDomain(doc: *DocumentHTML) ![]const u8 {
var s: ?*String = undefined;
const err = documentHTMLVtable(doc).get_domain.?(doc, &s);
try DOMErr(err);
if (s == null) return "";
return strToData(s.?);
}
pub inline fn documentHTMLGetReferrer(doc: *DocumentHTML) ![]const u8 {
var s: ?*String = undefined;
const err = documentHTMLVtable(doc).get_referrer.?(doc, &s);

View File

@@ -95,8 +95,6 @@ pub const Page = struct {
state_pool: *std.heap.MemoryPool(State),
polyfill_loader: polyfill.Loader = .{},
pub fn init(self: *Page, arena: Allocator, session: *Session) !void {
const browser = session.browser;
self.* = .{
@@ -119,8 +117,10 @@ pub const Page = struct {
}),
.main_context = undefined,
};
self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader));
try polyfill.preload(self.arena, self.main_context);
self.main_context = try session.executor.createJsContext(&self.window, self, self, true);
// load polyfills
try polyfill.load(self.arena, self.main_context);
// message loop must run only non-test env
if (comptime !builtin.is_test) {
@@ -142,7 +142,7 @@ pub const Page = struct {
}
// dump writes the page content into the given file.
pub fn dump(self: *const Page, opts: Dump.Opts, out: std.fs.File) !void {
pub fn dump(self: *const Page, out: std.fs.File) !void {
if (self.raw_data) |raw_data| {
// raw_data was set if the document was not HTML, dump the data content only.
return try out.writeAll(raw_data);
@@ -150,7 +150,7 @@ pub const Page = struct {
// if the page has a pointer to a document, dumps the HTML.
const doc = parser.documentHTMLToDocument(self.window.document);
try Dump.writeHTML(doc, opts, out);
try Dump.writeHTML(doc, out);
}
pub fn fetchModuleSource(ctx: *anyopaque, src: []const u8) !?[]const u8 {
@@ -367,6 +367,18 @@ pub const Page = struct {
const e = parser.nodeToElement(current);
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
// if (tag == .undef) {
// const tag_name = try parser.nodeLocalName(@ptrCast(e));
// const custom_elements = &self.window.custom_elements;
// if (custom_elements._get(tag_name)) |construct| {
// try construct.printFunc();
// // This is just here for testing for now.
// // var result: Env.Function.Result = undefined;
// // _ = try construct.newInstance(*parser.Element, &result);
// log.info(.browser, "Registered WebComponent Found", .{ .element_name = tag_name });
// }
// }
if (tag != .script) {
// ignore non-js script.
continue;
@@ -1018,16 +1030,12 @@ const Script = struct {
.cacheable = cacheable,
});
const failed = blk: {
switch (self.kind) {
.javascript => _ = page.main_context.eval(body, src) catch break :blk true,
// We don't care about waiting for the evaluation here.
.module => _ = page.main_context.module(body, src, cacheable) catch break :blk true,
}
break :blk false;
const result = switch (self.kind) {
.javascript => page.main_context.eval(body, src),
.module => page.main_context.module(body, src, cacheable),
};
if (failed) {
result catch {
if (page.delayed_navigation) {
return error.Terminated;
}
@@ -1042,8 +1050,7 @@ const Script = struct {
try self.executeCallback("onerror", page);
return error.JsErr;
}
};
try self.executeCallback("onload", page);
}

View File

@@ -16,6 +16,8 @@ test "Browser.fetch" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try @import("polyfill.zig").load(testing.allocator, runner.page.main_context);
try runner.testCases(&.{
.{
\\ var ok = false;

View File

@@ -23,98 +23,25 @@ const log = @import("../../log.zig");
const Allocator = std.mem.Allocator;
const Env = @import("../env.zig").Env;
pub const Loader = struct {
state: enum { empty, loading } = .empty,
done: struct {
fetch: bool = false,
webcomponents: bool = false,
} = .{},
fn load(self: *Loader, comptime name: []const u8, source: []const u8, js_context: *Env.JsContext) void {
var try_catch: Env.TryCatch = undefined;
try_catch.init(js_context);
defer try_catch.deinit();
self.state = .loading;
defer self.state = .empty;
log.debug(.js, "polyfill load", .{ .name = name });
_ = js_context.exec(source, name) catch |err| {
log.fatal(.app, "polyfill error", .{
.name = name,
.err = try_catch.err(js_context.call_arena) catch @errorName(err) orelse @errorName(err),
});
};
@field(self.done, name) = true;
}
pub fn missing(self: *Loader, name: []const u8, js_context: *Env.JsContext) bool {
// Avoid recursive calls during polyfill loading.
if (self.state == .loading) {
return false;
}
if (!self.done.fetch and isFetch(name)) {
const source = @import("fetch.zig").source;
self.load("fetch", source, js_context);
// We return false here: We want v8 to continue the calling chain
// to finally find the polyfill we just inserted. If we want to
// return false and stops the call chain, we have to use
// `info.GetReturnValue.Set()` function, or `undefined` will be
// returned immediately.
return false;
}
if (!self.done.webcomponents and isWebcomponents(name)) {
const source = @import("webcomponents.zig").source;
self.load("webcomponents", source, js_context);
// We return false here: We want v8 to continue the calling chain
// to finally find the polyfill we just inserted. If we want to
// return false and stops the call chain, we have to use
// `info.GetReturnValue.Set()` function, or `undefined` will be
// returned immediately.
return false;
}
if (comptime builtin.mode == .Debug) {
log.debug(.unknown_prop, "unkown global property", .{
.info = "but the property can exist in pure JS",
.property = name,
});
}
return false;
}
fn isFetch(name: []const u8) bool {
if (std.mem.eql(u8, name, "fetch")) return true;
if (std.mem.eql(u8, name, "Request")) return true;
if (std.mem.eql(u8, name, "Response")) return true;
if (std.mem.eql(u8, name, "Headers")) return true;
return false;
}
fn isWebcomponents(name: []const u8) bool {
if (std.mem.eql(u8, name, "customElements")) return true;
return false;
}
const modules = [_]struct {
name: []const u8,
source: []const u8,
}{
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
};
pub fn preload(allocator: Allocator, js_context: *Env.JsContext) !void {
pub fn load(allocator: Allocator, js_context: *Env.JsContext) !void {
var try_catch: Env.TryCatch = undefined;
try_catch.init(js_context);
defer try_catch.deinit();
const name = "webcomponents-pre";
const source = @import("webcomponents.zig").pre;
_ = js_context.exec(source, name) catch |err| {
if (try try_catch.err(allocator)) |msg| {
defer allocator.free(msg);
log.fatal(.app, "polyfill error", .{ .name = name, .err = msg });
}
return err;
};
for (modules) |m| {
_ = js_context.exec(m.source, m.name) catch |err| {
if (try try_catch.err(allocator)) |msg| {
defer allocator.free(msg);
log.fatal(.app, "polyfill error", .{ .name = m.name, .err = msg });
}
return err;
};
}
}

View File

@@ -1,61 +0,0 @@
/**
@license @nocompile
Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
(function(){/*
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found
at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
Google as part of the polymer project is also subject to an additional IP
rights grant found at http://polymer.github.io/PATENTS.txt
*/
'use strict';var n=window.Document.prototype.createElement,p=window.Document.prototype.createElementNS,aa=window.Document.prototype.importNode,ba=window.Document.prototype.prepend,ca=window.Document.prototype.append,da=window.DocumentFragment.prototype.prepend,ea=window.DocumentFragment.prototype.append,q=window.Node.prototype.cloneNode,r=window.Node.prototype.appendChild,t=window.Node.prototype.insertBefore,u=window.Node.prototype.removeChild,v=window.Node.prototype.replaceChild,w=Object.getOwnPropertyDescriptor(window.Node.prototype,
"textContent"),y=window.Element.prototype.attachShadow,z=Object.getOwnPropertyDescriptor(window.Element.prototype,"innerHTML"),A=window.Element.prototype.getAttribute,B=window.Element.prototype.setAttribute,C=window.Element.prototype.removeAttribute,D=window.Element.prototype.toggleAttribute,E=window.Element.prototype.getAttributeNS,F=window.Element.prototype.setAttributeNS,G=window.Element.prototype.removeAttributeNS,H=window.Element.prototype.insertAdjacentElement,fa=window.Element.prototype.insertAdjacentHTML,
ha=window.Element.prototype.prepend,ia=window.Element.prototype.append,ja=window.Element.prototype.before,ka=window.Element.prototype.after,la=window.Element.prototype.replaceWith,ma=window.Element.prototype.remove,na=window.HTMLElement,I=Object.getOwnPropertyDescriptor(window.HTMLElement.prototype,"innerHTML"),oa=window.HTMLElement.prototype.insertAdjacentElement,pa=window.HTMLElement.prototype.insertAdjacentHTML;var qa=new Set;"annotation-xml color-profile font-face font-face-src font-face-uri font-face-format font-face-name missing-glyph".split(" ").forEach(function(a){return qa.add(a)});function ra(a){var b=qa.has(a);a=/^[a-z][.0-9_a-z]*-[-.0-9_a-z]*$/.test(a);return!b&&a}var sa=document.contains?document.contains.bind(document):document.documentElement.contains.bind(document.documentElement);
function J(a){var b=a.isConnected;if(void 0!==b)return b;if(sa(a))return!0;for(;a&&!(a.__CE_isImportDocument||a instanceof Document);)a=a.parentNode||(window.ShadowRoot&&a instanceof ShadowRoot?a.host:void 0);return!(!a||!(a.__CE_isImportDocument||a instanceof Document))}function K(a){var b=a.children;if(b)return Array.prototype.slice.call(b);b=[];for(a=a.firstChild;a;a=a.nextSibling)a.nodeType===Node.ELEMENT_NODE&&b.push(a);return b}
function L(a,b){for(;b&&b!==a&&!b.nextSibling;)b=b.parentNode;return b&&b!==a?b.nextSibling:null}
function M(a,b,d){for(var f=a;f;){if(f.nodeType===Node.ELEMENT_NODE){var c=f;b(c);var e=c.localName;if("link"===e&&"import"===c.getAttribute("rel")){f=c.import;void 0===d&&(d=new Set);if(f instanceof Node&&!d.has(f))for(d.add(f),f=f.firstChild;f;f=f.nextSibling)M(f,b,d);f=L(a,c);continue}else if("template"===e){f=L(a,c);continue}if(c=c.__CE_shadowRoot)for(c=c.firstChild;c;c=c.nextSibling)M(c,b,d)}f=f.firstChild?f.firstChild:L(a,f)}};function N(){var a=!(null===O||void 0===O||!O.noDocumentConstructionObserver),b=!(null===O||void 0===O||!O.shadyDomFastWalk);this.m=[];this.g=[];this.j=!1;this.shadyDomFastWalk=b;this.I=!a}function P(a,b,d,f){var c=window.ShadyDOM;if(a.shadyDomFastWalk&&c&&c.inUse){if(b.nodeType===Node.ELEMENT_NODE&&d(b),b.querySelectorAll)for(a=c.nativeMethods.querySelectorAll.call(b,"*"),b=0;b<a.length;b++)d(a[b])}else M(b,d,f)}function ta(a,b){a.j=!0;a.m.push(b)}function ua(a,b){a.j=!0;a.g.push(b)}
function Q(a,b){a.j&&P(a,b,function(d){return R(a,d)})}function R(a,b){if(a.j&&!b.__CE_patched){b.__CE_patched=!0;for(var d=0;d<a.m.length;d++)a.m[d](b);for(d=0;d<a.g.length;d++)a.g[d](b)}}function S(a,b){var d=[];P(a,b,function(c){return d.push(c)});for(b=0;b<d.length;b++){var f=d[b];1===f.__CE_state?a.connectedCallback(f):T(a,f)}}function U(a,b){var d=[];P(a,b,function(c){return d.push(c)});for(b=0;b<d.length;b++){var f=d[b];1===f.__CE_state&&a.disconnectedCallback(f)}}
function V(a,b,d){d=void 0===d?{}:d;var f=d.J,c=d.upgrade||function(g){return T(a,g)},e=[];P(a,b,function(g){a.j&&R(a,g);if("link"===g.localName&&"import"===g.getAttribute("rel")){var h=g.import;h instanceof Node&&(h.__CE_isImportDocument=!0,h.__CE_registry=document.__CE_registry);h&&"complete"===h.readyState?h.__CE_documentLoadHandled=!0:g.addEventListener("load",function(){var k=g.import;if(!k.__CE_documentLoadHandled){k.__CE_documentLoadHandled=!0;var l=new Set;f&&(f.forEach(function(m){return l.add(m)}),
l.delete(k));V(a,k,{J:l,upgrade:c})}})}else e.push(g)},f);for(b=0;b<e.length;b++)c(e[b])}
function T(a,b){try{var d=b.ownerDocument,f=d.__CE_registry;var c=f&&(d.defaultView||d.__CE_isImportDocument)?W(f,b.localName):void 0;if(c&&void 0===b.__CE_state){c.constructionStack.push(b);try{try{if(new c.constructorFunction!==b)throw Error("The custom element constructor did not produce the element being upgraded.");}finally{c.constructionStack.pop()}}catch(k){throw b.__CE_state=2,k;}b.__CE_state=1;b.__CE_definition=c;if(c.attributeChangedCallback&&b.hasAttributes()){var e=c.observedAttributes;
for(c=0;c<e.length;c++){var g=e[c],h=b.getAttribute(g);null!==h&&a.attributeChangedCallback(b,g,null,h,null)}}J(b)&&a.connectedCallback(b)}}catch(k){X(k)}}N.prototype.connectedCallback=function(a){var b=a.__CE_definition;if(b.connectedCallback)try{b.connectedCallback.call(a)}catch(d){X(d)}};N.prototype.disconnectedCallback=function(a){var b=a.__CE_definition;if(b.disconnectedCallback)try{b.disconnectedCallback.call(a)}catch(d){X(d)}};
N.prototype.attributeChangedCallback=function(a,b,d,f,c){var e=a.__CE_definition;if(e.attributeChangedCallback&&-1<e.observedAttributes.indexOf(b))try{e.attributeChangedCallback.call(a,b,d,f,c)}catch(g){X(g)}};
function va(a,b,d,f){var c=b.__CE_registry;if(c&&(null===f||"http://www.w3.org/1999/xhtml"===f)&&(c=W(c,d)))try{var e=new c.constructorFunction;if(void 0===e.__CE_state||void 0===e.__CE_definition)throw Error("Failed to construct '"+d+"': The returned value was not constructed with the HTMLElement constructor.");if("http://www.w3.org/1999/xhtml"!==e.namespaceURI)throw Error("Failed to construct '"+d+"': The constructed element's namespace must be the HTML namespace.");if(e.hasAttributes())throw Error("Failed to construct '"+
d+"': The constructed element must not have any attributes.");if(null!==e.firstChild)throw Error("Failed to construct '"+d+"': The constructed element must not have any children.");if(null!==e.parentNode)throw Error("Failed to construct '"+d+"': The constructed element must not have a parent node.");if(e.ownerDocument!==b)throw Error("Failed to construct '"+d+"': The constructed element's owner document is incorrect.");if(e.localName!==d)throw Error("Failed to construct '"+d+"': The constructed element's local name is incorrect.");
return e}catch(g){return X(g),b=null===f?n.call(b,d):p.call(b,f,d),Object.setPrototypeOf(b,HTMLUnknownElement.prototype),b.__CE_state=2,b.__CE_definition=void 0,R(a,b),b}b=null===f?n.call(b,d):p.call(b,f,d);R(a,b);return b}
function X(a){var b="",d="",f=0,c=0;a instanceof Error?(b=a.message,d=a.sourceURL||a.fileName||"",f=a.line||a.lineNumber||0,c=a.column||a.columnNumber||0):b="Uncaught "+String(a);var e=void 0;void 0===ErrorEvent.prototype.initErrorEvent?e=new ErrorEvent("error",{cancelable:!0,message:b,filename:d,lineno:f,colno:c,error:a}):(e=document.createEvent("ErrorEvent"),e.initErrorEvent("error",!1,!0,b,d,f),e.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{configurable:!0,get:function(){return!0}})});
void 0===e.error&&Object.defineProperty(e,"error",{configurable:!0,enumerable:!0,get:function(){return a}});window.dispatchEvent(e);e.defaultPrevented||console.error(a)};function wa(){var a=this;this.g=void 0;this.F=new Promise(function(b){a.l=b})}wa.prototype.resolve=function(a){if(this.g)throw Error("Already resolved.");this.g=a;this.l(a)};function xa(a){var b=document;this.l=void 0;this.h=a;this.g=b;V(this.h,this.g);"loading"===this.g.readyState&&(this.l=new MutationObserver(this.G.bind(this)),this.l.observe(this.g,{childList:!0,subtree:!0}))}function ya(a){a.l&&a.l.disconnect()}xa.prototype.G=function(a){var b=this.g.readyState;"interactive"!==b&&"complete"!==b||ya(this);for(b=0;b<a.length;b++)for(var d=a[b].addedNodes,f=0;f<d.length;f++)V(this.h,d[f])};function Y(a){this.s=new Map;this.u=new Map;this.C=new Map;this.A=!1;this.B=new Map;this.o=function(b){return b()};this.i=!1;this.v=[];this.h=a;this.D=a.I?new xa(a):void 0}Y.prototype.H=function(a,b){var d=this;if(!(b instanceof Function))throw new TypeError("Custom element constructor getters must be functions.");za(this,a);this.s.set(a,b);this.v.push(a);this.i||(this.i=!0,this.o(function(){return Aa(d)}))};
Y.prototype.define=function(a,b){var d=this;if(!(b instanceof Function))throw new TypeError("Custom element constructors must be functions.");za(this,a);Ba(this,a,b);this.v.push(a);this.i||(this.i=!0,this.o(function(){return Aa(d)}))};function za(a,b){if(!ra(b))throw new SyntaxError("The element name '"+b+"' is not valid.");if(W(a,b))throw Error("A custom element with name '"+(b+"' has already been defined."));if(a.A)throw Error("A custom element is already being defined.");}
function Ba(a,b,d){a.A=!0;var f;try{var c=d.prototype;if(!(c instanceof Object))throw new TypeError("The custom element constructor's prototype is not an object.");var e=function(m){var x=c[m];if(void 0!==x&&!(x instanceof Function))throw Error("The '"+m+"' callback must be a function.");return x};var g=e("connectedCallback");var h=e("disconnectedCallback");var k=e("adoptedCallback");var l=(f=e("attributeChangedCallback"))&&d.observedAttributes||[]}catch(m){throw m;}finally{a.A=!1}d={localName:b,
constructorFunction:d,connectedCallback:g,disconnectedCallback:h,adoptedCallback:k,attributeChangedCallback:f,observedAttributes:l,constructionStack:[]};a.u.set(b,d);a.C.set(d.constructorFunction,d);return d}Y.prototype.upgrade=function(a){V(this.h,a)};
function Aa(a){if(!1!==a.i){a.i=!1;for(var b=[],d=a.v,f=new Map,c=0;c<d.length;c++)f.set(d[c],[]);V(a.h,document,{upgrade:function(k){if(void 0===k.__CE_state){var l=k.localName,m=f.get(l);m?m.push(k):a.u.has(l)&&b.push(k)}}});for(c=0;c<b.length;c++)T(a.h,b[c]);for(c=0;c<d.length;c++){for(var e=d[c],g=f.get(e),h=0;h<g.length;h++)T(a.h,g[h]);(e=a.B.get(e))&&e.resolve(void 0)}d.length=0}}Y.prototype.get=function(a){if(a=W(this,a))return a.constructorFunction};
Y.prototype.whenDefined=function(a){if(!ra(a))return Promise.reject(new SyntaxError("'"+a+"' is not a valid custom element name."));var b=this.B.get(a);if(b)return b.F;b=new wa;this.B.set(a,b);var d=this.u.has(a)||this.s.has(a);a=-1===this.v.indexOf(a);d&&a&&b.resolve(void 0);return b.F};Y.prototype.polyfillWrapFlushCallback=function(a){this.D&&ya(this.D);var b=this.o;this.o=function(d){return a(function(){return b(d)})}};
function W(a,b){var d=a.u.get(b);if(d)return d;if(d=a.s.get(b)){a.s.delete(b);try{return Ba(a,b,d())}catch(f){X(f)}}}Y.prototype.define=Y.prototype.define;Y.prototype.upgrade=Y.prototype.upgrade;Y.prototype.get=Y.prototype.get;Y.prototype.whenDefined=Y.prototype.whenDefined;Y.prototype.polyfillDefineLazy=Y.prototype.H;Y.prototype.polyfillWrapFlushCallback=Y.prototype.polyfillWrapFlushCallback;function Z(a,b,d){function f(c){return function(e){for(var g=[],h=0;h<arguments.length;++h)g[h]=arguments[h];h=[];for(var k=[],l=0;l<g.length;l++){var m=g[l];m instanceof Element&&J(m)&&k.push(m);if(m instanceof DocumentFragment)for(m=m.firstChild;m;m=m.nextSibling)h.push(m);else h.push(m)}c.apply(this,g);for(g=0;g<k.length;g++)U(a,k[g]);if(J(this))for(g=0;g<h.length;g++)k=h[g],k instanceof Element&&S(a,k)}}void 0!==d.prepend&&(b.prepend=f(d.prepend));void 0!==d.append&&(b.append=f(d.append))};function Ca(a){Document.prototype.createElement=function(b){return va(a,this,b,null)};Document.prototype.importNode=function(b,d){b=aa.call(this,b,!!d);this.__CE_registry?V(a,b):Q(a,b);return b};Document.prototype.createElementNS=function(b,d){return va(a,this,d,b)};Z(a,Document.prototype,{prepend:ba,append:ca})};function Da(a){function b(f){return function(c){for(var e=[],g=0;g<arguments.length;++g)e[g]=arguments[g];g=[];for(var h=[],k=0;k<e.length;k++){var l=e[k];l instanceof Element&&J(l)&&h.push(l);if(l instanceof DocumentFragment)for(l=l.firstChild;l;l=l.nextSibling)g.push(l);else g.push(l)}f.apply(this,e);for(e=0;e<h.length;e++)U(a,h[e]);if(J(this))for(e=0;e<g.length;e++)h=g[e],h instanceof Element&&S(a,h)}}var d=Element.prototype;void 0!==ja&&(d.before=b(ja));void 0!==ka&&(d.after=b(ka));void 0!==la&&
(d.replaceWith=function(f){for(var c=[],e=0;e<arguments.length;++e)c[e]=arguments[e];e=[];for(var g=[],h=0;h<c.length;h++){var k=c[h];k instanceof Element&&J(k)&&g.push(k);if(k instanceof DocumentFragment)for(k=k.firstChild;k;k=k.nextSibling)e.push(k);else e.push(k)}h=J(this);la.apply(this,c);for(c=0;c<g.length;c++)U(a,g[c]);if(h)for(U(a,this),c=0;c<e.length;c++)g=e[c],g instanceof Element&&S(a,g)});void 0!==ma&&(d.remove=function(){var f=J(this);ma.call(this);f&&U(a,this)})};function Ea(a){function b(c,e){Object.defineProperty(c,"innerHTML",{enumerable:e.enumerable,configurable:!0,get:e.get,set:function(g){var h=this,k=void 0;J(this)&&(k=[],P(a,this,function(x){x!==h&&k.push(x)}));e.set.call(this,g);if(k)for(var l=0;l<k.length;l++){var m=k[l];1===m.__CE_state&&a.disconnectedCallback(m)}this.ownerDocument.__CE_registry?V(a,this):Q(a,this);return g}})}function d(c,e){c.insertAdjacentElement=function(g,h){var k=J(h);g=e.call(this,g,h);k&&U(a,h);J(g)&&S(a,h);return g}}function f(c,
e){function g(h,k){for(var l=[];h!==k;h=h.nextSibling)l.push(h);for(k=0;k<l.length;k++)V(a,l[k])}c.insertAdjacentHTML=function(h,k){h=h.toLowerCase();if("beforebegin"===h){var l=this.previousSibling;e.call(this,h,k);g(l||this.parentNode.firstChild,this)}else if("afterbegin"===h)l=this.firstChild,e.call(this,h,k),g(this.firstChild,l);else if("beforeend"===h)l=this.lastChild,e.call(this,h,k),g(l||this.firstChild,null);else if("afterend"===h)l=this.nextSibling,e.call(this,h,k),g(this.nextSibling,l);
else throw new SyntaxError("The value provided ("+String(h)+") is not one of 'beforebegin', 'afterbegin', 'beforeend', or 'afterend'.");}}y&&(Element.prototype.attachShadow=function(c){c=y.call(this,c);if(a.j&&!c.__CE_patched){c.__CE_patched=!0;for(var e=0;e<a.m.length;e++)a.m[e](c)}return this.__CE_shadowRoot=c});z&&z.get?b(Element.prototype,z):I&&I.get?b(HTMLElement.prototype,I):ua(a,function(c){b(c,{enumerable:!0,configurable:!0,get:function(){return q.call(this,!0).innerHTML},set:function(e){var g=
"template"===this.localName,h=g?this.content:this,k=p.call(document,this.namespaceURI,this.localName);for(k.innerHTML=e;0<h.childNodes.length;)u.call(h,h.childNodes[0]);for(e=g?k.content:k;0<e.childNodes.length;)r.call(h,e.childNodes[0])}})});Element.prototype.setAttribute=function(c,e){if(1!==this.__CE_state)return B.call(this,c,e);var g=A.call(this,c);B.call(this,c,e);e=A.call(this,c);a.attributeChangedCallback(this,c,g,e,null)};Element.prototype.setAttributeNS=function(c,e,g){if(1!==this.__CE_state)return F.call(this,
c,e,g);var h=E.call(this,c,e);F.call(this,c,e,g);g=E.call(this,c,e);a.attributeChangedCallback(this,e,h,g,c)};Element.prototype.removeAttribute=function(c){if(1!==this.__CE_state)return C.call(this,c);var e=A.call(this,c);C.call(this,c);null!==e&&a.attributeChangedCallback(this,c,e,null,null)};D&&(Element.prototype.toggleAttribute=function(c,e){if(1!==this.__CE_state)return D.call(this,c,e);var g=A.call(this,c),h=null!==g;e=D.call(this,c,e);h!==e&&a.attributeChangedCallback(this,c,g,e?"":null,null);
return e});Element.prototype.removeAttributeNS=function(c,e){if(1!==this.__CE_state)return G.call(this,c,e);var g=E.call(this,c,e);G.call(this,c,e);var h=E.call(this,c,e);g!==h&&a.attributeChangedCallback(this,e,g,h,c)};oa?d(HTMLElement.prototype,oa):H&&d(Element.prototype,H);pa?f(HTMLElement.prototype,pa):fa&&f(Element.prototype,fa);Z(a,Element.prototype,{prepend:ha,append:ia});Da(a)};var Fa={};function Ga(a){function b(){var d=this.constructor;var f=document.__CE_registry.C.get(d);if(!f)throw Error("Failed to construct a custom element: The constructor was not registered with `customElements`.");var c=f.constructionStack;if(0===c.length)return c=n.call(document,f.localName),Object.setPrototypeOf(c,d.prototype),c.__CE_state=1,c.__CE_definition=f,R(a,c),c;var e=c.length-1,g=c[e];if(g===Fa)throw Error("Failed to construct '"+f.localName+"': This element was already constructed.");c[e]=Fa;
Object.setPrototypeOf(g,d.prototype);R(a,g);return g}b.prototype=na.prototype;Object.defineProperty(HTMLElement.prototype,"constructor",{writable:!0,configurable:!0,enumerable:!1,value:b});window.HTMLElement=b};function Ha(a){function b(d,f){Object.defineProperty(d,"textContent",{enumerable:f.enumerable,configurable:!0,get:f.get,set:function(c){if(this.nodeType===Node.TEXT_NODE)f.set.call(this,c);else{var e=void 0;if(this.firstChild){var g=this.childNodes,h=g.length;if(0<h&&J(this)){e=Array(h);for(var k=0;k<h;k++)e[k]=g[k]}}f.set.call(this,c);if(e)for(c=0;c<e.length;c++)U(a,e[c])}}})}Node.prototype.insertBefore=function(d,f){if(d instanceof DocumentFragment){var c=K(d);d=t.call(this,d,f);if(J(this))for(f=
0;f<c.length;f++)S(a,c[f]);return d}c=d instanceof Element&&J(d);f=t.call(this,d,f);c&&U(a,d);J(this)&&S(a,d);return f};Node.prototype.appendChild=function(d){if(d instanceof DocumentFragment){var f=K(d);d=r.call(this,d);if(J(this))for(var c=0;c<f.length;c++)S(a,f[c]);return d}f=d instanceof Element&&J(d);c=r.call(this,d);f&&U(a,d);J(this)&&S(a,d);return c};Node.prototype.cloneNode=function(d){d=q.call(this,!!d);this.ownerDocument.__CE_registry?V(a,d):Q(a,d);return d};Node.prototype.removeChild=function(d){var f=
d instanceof Element&&J(d),c=u.call(this,d);f&&U(a,d);return c};Node.prototype.replaceChild=function(d,f){if(d instanceof DocumentFragment){var c=K(d);d=v.call(this,d,f);if(J(this))for(U(a,f),f=0;f<c.length;f++)S(a,c[f]);return d}c=d instanceof Element&&J(d);var e=v.call(this,d,f),g=J(this);g&&U(a,f);c&&U(a,d);g&&S(a,d);return e};w&&w.get?b(Node.prototype,w):ta(a,function(d){b(d,{enumerable:!0,configurable:!0,get:function(){for(var f=[],c=this.firstChild;c;c=c.nextSibling)c.nodeType!==Node.COMMENT_NODE&&
f.push(c.textContent);return f.join("")},set:function(f){for(;this.firstChild;)u.call(this,this.firstChild);null!=f&&""!==f&&r.call(this,document.createTextNode(f))}})})};var O=window.customElements;function Ia(){var a=new N;Ga(a);Ca(a);Z(a,DocumentFragment.prototype,{prepend:da,append:ea});Ha(a);Ea(a);window.CustomElementRegistry=Y;a=new Y(a);document.__CE_registry=a;Object.defineProperty(window,"customElements",{configurable:!0,enumerable:!0,value:a})}O&&!O.forcePolyfill&&"function"==typeof O.define&&"function"==typeof O.get||Ia();window.__CE_installPolyfill=Ia;/*
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
}).call(this);

View File

@@ -1,65 +0,0 @@
// webcomponents.js code comes from
// https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs
//
// The original code source is available in a "BSD style license".
//
// This is the `webcomponents-ce.js` bundle
pub const source = @embedFile("webcomponents.js");
// The main webcomponents.js is lazilly loaded when window.customElements is
// called. But, if you look at the test below, you'll notice that we declare
// our custom element (LightPanda) before we call `customElements.define`. We
// _have_ to declare it before we can register it.
// That causes an issue, because the LightPanda class extends HTMLElement, which
// hasn't been monkeypatched by the polyfill yet. If you were to try it as-is
// you'd get an "Illegal Constructor", because that's what the Zig HTMLElement
// constructor does (and that's correct).
// However, once HTMLElement is monkeypatched, it'll work. One simple solution
// is to run the webcomponents.js polyfill proactively on each page, ensuring
// that HTMLElement is monkeypatched before any other JavaScript is run. But
// that adds _a lot_ of overhead.
// So instead of always running the [large and intrusive] webcomponents.js
// polyfill, we'll always run this little snippet. It wraps the HTMLElement
// constructor. When the Lightpanda class is created, it'll extend our little
// wrapper. But, unlike the Zig default constructor which throws, our code
// calls the "real" constructor. That might seem like the same thing, but by the
// time our wrapper is called, the webcomponents.js polyfill will have been
// loaded and the "real" constructor will be the monkeypatched version.
// TL;DR creates a layer of indirection for the constructor, so that, when it's
// actually instantiated, the webcomponents.js polyfill will have been loaded.
pub const pre =
\\ (() => {
\\ const HE = window.HTMLElement;
\\ const b = function() { return HE.prototype.constructor.call(this); }
\\ b.prototype = HE.prototype;
\\ window.HTMLElement = b;
\\ })();
;
const testing = @import("../../testing.zig");
test "Browser.webcomponents" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{ .html = "<div id=main></div>" });
defer runner.deinit();
try @import("polyfill.zig").preload(testing.allocator, runner.page.main_context);
try runner.testCases(&.{
.{
\\ class LightPanda extends HTMLElement {
\\ constructor() {
\\ super();
\\ }
\\ connectedCallback() {
\\ this.append('connected');
\\ }
\\ }
\\ window.customElements.define("lightpanda-test", LightPanda);
\\ const main = document.getElementById('main');
\\ main.appendChild(document.createElement('lightpanda-test'));
,
null,
},
.{ "main.innerHTML", "<lightpanda-test>connected</lightpanda-test>" },
}, .{});
}

View File

@@ -89,17 +89,7 @@ pub const URL = struct {
raw = if (url == .url) url_str else try arena.dupe(u8, url_str);
}
const uri = std.Uri.parse(raw.?) catch blk: {
if (!std.mem.endsWith(u8, raw.?, "://")) {
return error.TypeError;
}
// schema only is valid!
break :blk std.Uri{
.scheme = raw.?[0 .. raw.?.len - 3],
.host = .{ .percent_encoded = "" },
};
};
const uri = std.Uri.parse(raw.?) catch return error.TypeError;
return init(arena, uri);
}
@@ -239,6 +229,19 @@ pub const URL = struct {
pub fn _toJSON(self: *URL, page: *Page) ![]const u8 {
return self.get_href(page);
}
pub fn set_pathname(self: *URL, fragment: []const u8, page: *Page) !void {
// pathname must always start with a '/';
const real_path = blk: {
if (std.mem.startsWith(u8, fragment, "/")) {
break :blk try page.arena.dupe(u8, fragment);
} else {
break :blk try std.fmt.allocPrint(page.arena, "/{s}", .{fragment});
}
};
self.uri.path = .{ .percent_encoded = real_path };
}
};
// uriComponentNullStr converts an optional std.Uri.Component to string value.
@@ -568,14 +571,6 @@ test "Browser.URL" {
.{ "var url = new URL('over?9000', 'https://lightpanda.io')", null },
.{ "url.href", "https://lightpanda.io/over?9000" },
}, .{});
try runner.testCases(&.{
.{ "let sk = new URL('sveltekit-internal://')", null },
.{ "sk.protocol", "sveltekit-internal:" },
.{ "sk.host", "" },
.{ "sk.hostname", "" },
.{ "sk.href", "sveltekit-internal://" },
}, .{});
}
test "Browser.URLSearchParams" {

View File

@@ -16,8 +16,6 @@
// 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/>.
// Currently not used. Relying on polyfill instead
const std = @import("std");
const log = @import("../../log.zig");
const v8 = @import("v8");

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -16,7 +16,8 @@
// 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 CustomElementRegistry = @import("custom_element_registry.zig").CustomElementRegistry;
pub const Interfaces = .{
@import("TextDecoder.zig"),
@import("TextEncoder.zig"),
CustomElementRegistry,
};

View File

@@ -31,7 +31,7 @@ pub const XMLHttpRequestEventTarget = struct {
pub const prototype = *EventTarget;
// Extend libdom event target for pure zig struct.
base: parser.EventTargetTBase = parser.EventTargetTBase{ .internal_target_type = .xhr },
base: parser.EventTargetTBase = parser.EventTargetTBase{},
onloadstart_cbk: ?Function = null,
onprogress_cbk: ?Function = null,

View File

@@ -36,9 +36,9 @@ pub const XMLSerializer = struct {
pub fn _serializeToString(_: *const XMLSerializer, root: *parser.Node, page: *Page) ![]const u8 {
var buf = std.ArrayList(u8).init(page.arena);
switch (try parser.nodeType(root)) {
.document => try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), .{}, buf.writer()),
.document => try dump.writeHTML(@as(*parser.Document, @ptrCast(root)), buf.writer()),
.document_type => try dump.writeDocType(@as(*parser.DocumentType, @ptrCast(root)), buf.writer()),
else => try dump.writeNode(root, .{}, buf.writer()),
else => try dump.writeNode(root, buf.writer()),
}
return buf.items;
}

View File

@@ -201,69 +201,49 @@ pub const Search = struct {
// (For now, we only support direct children)
pub const Writer = struct {
depth: i32,
exclude_root: bool,
root: *const Node,
opts: Opts,
node: *const Node,
registry: *Registry,
pub const Opts = struct {
depth: i32 = 0,
exclude_root: bool = false,
};
pub const Opts = struct {};
pub fn jsonStringify(self: *const Writer, w: anytype) !void {
if (self.exclude_root) {
_ = self.writeChildren(self.root, 1, w) catch |err| {
log.err(.cdp, "node writeChildren", .{ .err = err });
return error.OutOfMemory;
};
} else {
self.toJSON(self.root, 0, w) catch |err| {
// The only error our jsonStringify method can return is
// @TypeOf(w).Error. In other words, our code can't return its own
// error, we can only return a writer error. Kinda sucks.
log.err(.cdp, "node toJSON stringify", .{ .err = err });
return error.OutOfMemory;
};
}
self.toJSON(w) catch |err| {
// The only error our jsonStringify method can return is
// @TypeOf(w).Error. In other words, our code can't return its own
// error, we can only return a writer error. Kinda sucks.
log.err(.cdp, "json stringify", .{ .err = err });
return error.OutOfMemory;
};
}
fn toJSON(self: *const Writer, node: *const Node, depth: usize, w: anytype) !void {
fn toJSON(self: *const Writer, w: anytype) !void {
try w.beginObject();
try self.writeCommon(node, false, w);
try self.writeCommon(self.node, false, w);
try w.objectField("children");
const child_count = try self.writeChildren(node, depth, w);
try w.objectField("childNodeCount");
try w.write(child_count);
{
var registry = self.registry;
const child_nodes = try parser.nodeGetChildNodes(self.node._node);
const child_count = try parser.nodeListLength(child_nodes);
try w.endObject();
}
fn writeChildren(self: *const Writer, node: *const Node, depth: usize, w: anytype) anyerror!usize {
var registry = self.registry;
const child_nodes = try parser.nodeGetChildNodes(node._node);
const child_count = try parser.nodeListLength(child_nodes);
const full_child = self.depth < 0 or self.depth < depth;
var i: usize = 0;
try w.beginArray();
for (0..child_count) |_| {
const child = (try parser.nodeListItem(child_nodes, @intCast(i))) orelse break;
const child_node = try registry.register(child);
if (full_child) {
try self.toJSON(child_node, depth + 1, w);
} else {
var i: usize = 0;
try w.objectField("children");
try w.beginArray();
for (0..child_count) |_| {
const child = (try parser.nodeListItem(child_nodes, @intCast(i))) orelse break;
const child_node = try registry.register(child);
try w.beginObject();
try self.writeCommon(child_node, true, w);
try w.endObject();
i += 1;
}
try w.endArray();
i += 1;
try w.objectField("childNodeCount");
try w.write(i);
}
try w.endArray();
return i;
try w.endObject();
}
fn writeCommon(self: *const Writer, node: *const Node, include_child_count: bool, w: anytype) !void {
@@ -420,15 +400,14 @@ test "cdp Node: Writer" {
var registry = Registry.init(testing.allocator);
defer registry.deinit();
var doc = try testing.Document.init("<a id=a1></a><div id=d2><a id=a2></a></div>");
var doc = try testing.Document.init("<a id=a1></a><a id=a2></a>");
defer doc.deinit();
{
const node = try registry.register(doc.asNode());
const json = try std.json.stringifyAlloc(testing.allocator, Writer{
.root = node,
.depth = 0,
.exclude_root = false,
.node = node,
.opts = .{},
.registry = &registry,
}, .{});
defer testing.allocator.free(json);
@@ -466,9 +445,8 @@ test "cdp Node: Writer" {
{
const node = registry.lookup_by_id.get(1).?;
const json = try std.json.stringifyAlloc(testing.allocator, Writer{
.root = node,
.depth = 1,
.exclude_root = false,
.node = node,
.opts = .{},
.registry = &registry,
}, .{});
defer testing.allocator.free(json);
@@ -517,61 +495,4 @@ test "cdp Node: Writer" {
} },
}, json);
}
{
const node = registry.lookup_by_id.get(1).?;
const json = try std.json.stringifyAlloc(testing.allocator, Writer{
.root = node,
.depth = -1,
.exclude_root = true,
.registry = &registry,
}, .{});
defer testing.allocator.free(json);
try testing.expectJson(&.{ .{
.nodeId = 2,
.backendNodeId = 2,
.nodeType = 1,
.nodeName = "HEAD",
.localName = "head",
.nodeValue = "",
.childNodeCount = 0,
.documentURL = null,
.baseURL = null,
.xmlVersion = "",
.compatibilityMode = "NoQuirksMode",
.isScrollable = false,
.parentId = 1,
}, .{
.nodeId = 3,
.backendNodeId = 3,
.nodeType = 1,
.nodeName = "BODY",
.localName = "body",
.nodeValue = "",
.childNodeCount = 2,
.documentURL = null,
.baseURL = null,
.xmlVersion = "",
.compatibilityMode = "NoQuirksMode",
.isScrollable = false,
.children = &.{ .{
.nodeId = 4,
.localName = "a",
.childNodeCount = 0,
.parentId = 3,
}, .{
.nodeId = 5,
.localName = "div",
.childNodeCount = 1,
.parentId = 3,
.children = &.{.{
.nodeId = 6,
.localName = "a",
.childNodeCount = 0,
.parentId = 5,
}},
} },
} }, json);
}
}

View File

@@ -30,8 +30,6 @@ const Inspector = @import("../browser/env.zig").Env.Inspector;
const Incrementing = @import("../id.zig").Incrementing;
const Notification = @import("../notification.zig").Notification;
const polyfill = @import("../browser/polyfill/polyfill.zig");
pub const URL_BASE = "chrome://newtab/";
pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C";
@@ -322,10 +320,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
inspector: Inspector,
isolated_world: ?IsolatedWorld,
// Used to restore the proxy after the CDP session ends. If CDP never over-wrote it, it won't restore it (the first null).
// If the CDP is restoring it, but the original value was null, that's the 2nd null.
// If you only have 1 null it would be ambiguous, does null mean it shouldn't be restored, or should it be restored to null?
http_proxy_before: ??std.Uri = null,
const Self = @This();
@@ -406,11 +400,10 @@ pub fn BrowserContext(comptime CDP_T: type) type {
return &self.isolated_world.?;
}
pub fn nodeWriter(self: *Self, root: *const Node, opts: Node.Writer.Opts) Node.Writer {
pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer {
return .{
.root = root,
.depth = opts.depth,
.exclude_root = opts.exclude_root,
.node = node,
.opts = opts,
.registry = &self.node_registry,
};
}
@@ -561,10 +554,6 @@ const IsolatedWorld = struct {
executor: Env.ExecutionWorld,
grant_universal_access: bool,
// Polyfill loader for the isolated world.
// We want to load polyfill in the world's context.
polyfill_loader: polyfill.Loader = .{},
pub fn deinit(self: *IsolatedWorld) void {
self.executor.deinit();
}
@@ -580,13 +569,7 @@ const IsolatedWorld = struct {
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
if (self.executor.js_context != null) return error.Only1IsolatedContextSupported;
_ = try self.executor.createJsContext(
&page.window,
page,
{},
false,
Env.GlobalMissingCallback.init(&self.polyfill_loader),
);
_ = try self.executor.createJsContext(&page.window, page, {}, false);
}
};

View File

@@ -38,7 +38,6 @@ pub fn processMessage(cmd: anytype) !void {
scrollIntoViewIfNeeded,
getContentQuads,
getBoxModel,
requestChildNodes,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
@@ -54,25 +53,22 @@ pub fn processMessage(cmd: anytype) !void {
.scrollIntoViewIfNeeded => return scrollIntoViewIfNeeded(cmd),
.getContentQuads => return getContentQuads(cmd),
.getBoxModel => return getBoxModel(cmd),
.requestChildNodes => return requestChildNodes(cmd),
}
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument
fn getDocument(cmd: anytype) !void {
const Params = struct {
// CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome
depth: i32 = 3,
pierce: bool = false,
};
const params = try cmd.params(Params) orelse Params{};
// const params = (try cmd.params(struct {
// depth: ?u32 = null,
// pierce: ?bool = null,
// })) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
const doc = parser.documentHTMLToDocument(page.window.document);
const node = try bc.node_registry.register(parser.documentToNode(doc));
return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{});
return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{}) }, .{});
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
@@ -437,30 +433,6 @@ fn getBoxModel(cmd: anytype) !void {
} }, .{});
}
fn requestChildNodes(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: Node.Id,
depth: i32 = 1,
pierce: bool = false,
})) orelse return error.InvalidParams;
if (params.depth == 0) return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const session_id = bc.session_id orelse return error.SessionIdNotLoaded;
const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse {
return error.InvalidNode;
};
try cmd.sendEvent("DOM.setChildNodes", .{
.parentId = node.id,
.nodes = bc.nodeWriter(node, .{ .depth = params.depth, .exclude_root = true }),
}, .{
.session_id = session_id,
});
return cmd.sendResult(null, .{});
}
const testing = @import("../testing.zig");
test "cdp.dom: getSearchResults unknown search id" {

View File

@@ -29,7 +29,6 @@ pub fn processMessage(cmd: anytype) !void {
disable,
setCacheDisabled,
setExtraHTTPHeaders,
setUserAgentOverride,
deleteCookies,
clearBrowserCookies,
setCookie,
@@ -41,7 +40,6 @@ pub fn processMessage(cmd: anytype) !void {
.enable => return enable(cmd),
.disable => return disable(cmd),
.setCacheDisabled => return cmd.sendResult(null, .{}),
.setUserAgentOverride => return cmd.sendResult(null, .{}),
.setExtraHTTPHeaders => return setExtraHTTPHeaders(cmd),
.deleteCookies => return deleteCookies(cmd),
.clearBrowserCookies => return clearBrowserCookies(cmd),

View File

@@ -31,7 +31,6 @@ pub fn processMessage(cmd: anytype) !void {
addScriptToEvaluateOnNewDocument,
createIsolatedWorld,
navigate,
stopLoading,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
@@ -41,7 +40,6 @@ pub fn processMessage(cmd: anytype) !void {
.addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd),
.createIsolatedWorld => return createIsolatedWorld(cmd),
.navigate => return navigate(cmd),
.stopLoading => return cmd.sendResult(null, .{}),
}
}
@@ -288,7 +286,7 @@ pub fn pageCreated(bc: anytype, page: *Page) !void {
try isolated_world.createContext(page);
const polyfill = @import("../../browser/polyfill/polyfill.zig");
try polyfill.preload(bc.arena, &isolated_world.executor.js_context.?);
try polyfill.load(bc.arena, &isolated_world.executor.js_context.?);
}
}

View File

@@ -240,10 +240,6 @@ pub const Client = struct {
const proxy_type = self.proxy_type orelse return false;
return proxy_type == .forward;
}
fn isProxy(self: *const Client) bool {
return self.proxy_type != null;
}
};
const RequestOpts = struct {
@@ -287,12 +283,6 @@ const AsyncQueue = struct {
fn _check(self: *AsyncQueue, repeat_delay: *?u63) !void {
const client = self.client;
if (client.freeSlotCount() == 1) {
// always leave 1 free connection for sync requests
repeat_delay.* = 10 * std.time.ns_per_ms;
return;
}
const state = client.state_pool.acquireOrNull() orelse {
// re-run this function in 10 milliseconds
repeat_delay.* = 10 * std.time.ns_per_ms;
@@ -802,16 +792,27 @@ pub const Request = struct {
.conn = .{ .handler = async_handler, .protocol = .{ .plain = {} } },
};
if (self._client.isConnectProxy() and self._proxy_secure) {
log.warn(.http, "not implemented", .{ .feature = "async tls connect" });
if (self._client.isConnectProxy() and self._proxy_secure) log.warn(.http, "ASYNC TLS CONNECT no impl.", .{});
if (self._request_secure) {
if (self._connection_from_keepalive) {
// If the connection came from the keepalive pool, than we already
// have a TLS Connection.
async_handler.conn.protocol = .{ .encrypted = .{ .conn = &connection.tls.?.nonblocking } };
} else {
std.debug.assert(connection.tls == null);
async_handler.conn.protocol = .{
.handshake = tls.nonblock.Client.init(.{
.host = if (self._client.isConnectProxy()) self._request_host else self._connect_host, // looks wrong
.root_ca = self._client.root_ca,
.insecure_skip_verify = self._tls_verify_host == false,
.key_log_callback = tls.config.key_log.callback,
}),
};
}
}
if (self._connection_from_keepalive) {
if ((self._client.isProxy() and self._proxy_secure) or (!self._client.isForwardProxy() and self._request_secure)) {
// If the connection came from the keepalive pool, than we already have a TLS Connection.
async_handler.conn.protocol = .{ .encrypted = .{ .conn = &connection.tls.?.nonblocking } };
}
// We're already connected
// we're already connected
async_handler.pending_connect = false;
return async_handler.conn.connected();
}
@@ -1141,19 +1142,6 @@ fn AsyncHandler(comptime H: type) type {
result catch |err| return self.handleError("Connection failed", err);
const request = self.request;
if ((request._client.isProxy() and request._proxy_secure) or (!request._client.isForwardProxy() and request._request_secure)) {
std.debug.assert(request._connection.?.tls == null);
self.conn.protocol = .{
.handshake = tls.nonblock.Client.init(.{
.host = if (request._client.isConnectProxy()) request._request_host else request._connect_host,
.root_ca = request._client.root_ca,
.insecure_skip_verify = request._tls_verify_host == false,
.key_log_callback = tls.config.key_log.callback,
}),
};
}
if (self.request.shouldProxyConnect()) {
self.state = .connect;
const header = self.request.buildConnectHeader() catch |err| {
@@ -3806,7 +3794,7 @@ const TestContext = struct {
errdefer loop.deinit();
var o = opts;
o.max_concurrent = 2;
o.max_concurrent = 1;
const client = try Client.init(testing.allocator, loop, o);
errdefer client.deinit();

View File

@@ -39,13 +39,12 @@ pub const Scope = enum {
unknown_prop,
web_api,
xhr,
polyfill,
};
const Opts = struct {
format: Format = if (is_debug) .pretty else .logfmt,
level: Level = if (is_debug) .info else .warn,
filter_scopes: []const Scope = &.{},
filter_scopes: []const Scope = &.{.unknown_prop},
};
pub var opts = Opts{};

View File

@@ -134,7 +134,7 @@ fn run(alloc: Allocator) !void {
// dump
if (opts.dump) {
try page.dump(.{ .exclude_scripts = opts.noscript }, std.io.getStdOut());
try page.dump(std.io.getStdOut());
}
},
else => unreachable,
@@ -212,7 +212,6 @@ const Command = struct {
url: []const u8,
dump: bool = false,
common: Common,
noscript: bool = false,
};
const Common = struct {
@@ -276,7 +275,6 @@ const Command = struct {
\\Options:
\\--dump Dumps document to stdout.
\\ Defaults to false.
\\--noscript Exclude <script> tags in dump. Defaults to false.
\\
++ common_options ++
\\
@@ -354,9 +352,6 @@ fn inferMode(opt: []const u8) ?App.RunMode {
if (std.mem.eql(u8, opt, "--dump")) {
return .fetch;
}
if (std.mem.eql(u8, opt, "--noscript")) {
return .fetch;
}
if (std.mem.startsWith(u8, opt, "--") == false) {
return .fetch;
}
@@ -442,7 +437,6 @@ fn parseFetchArgs(
args: *std.process.ArgIterator,
) !Command.Fetch {
var dump: bool = false;
var noscript: bool = true;
var url: ?[]const u8 = null;
var common: Command.Common = .{};
@@ -452,11 +446,6 @@ fn parseFetchArgs(
continue;
}
if (std.mem.eql(u8, "--noscript", opt)) {
noscript = true;
continue;
}
if (try parseCommonArg(allocator, opt, args, &common)) {
continue;
}
@@ -482,7 +471,6 @@ fn parseFetchArgs(
.url = url.?,
.dump = dump,
.common = common,
.noscript = noscript,
};
}

View File

@@ -126,7 +126,7 @@ fn run(
});
defer runner.deinit();
try polyfill.preload(arena, runner.page.main_context);
try polyfill.load(arena, runner.page.main_context);
// loop over the scripts.
const doc = parser.documentHTMLToDocument(runner.page.window.document);

View File

@@ -195,9 +195,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
var isolate = v8.Isolate.init(params);
errdefer isolate.deinit();
// This is the callback that runs whenever a module is dynamically imported.
isolate.setHostImportModuleDynamicallyCallback(JsContext.dynamicModuleCallback);
isolate.enter();
errdefer isolate.exit();
@@ -373,7 +370,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// when the handle_scope is freed.
// We also maintain our own "context_arena" which allows us to have
// all page related memory easily managed.
pub fn createJsContext(self: *ExecutionWorld, global: anytype, state: State, module_loader: anytype, enter: bool, global_callback: ?GlobalMissingCallback) !*JsContext {
pub fn createJsContext(self: *ExecutionWorld, global: anytype, state: State, module_loader: anytype, enter: bool) !*JsContext {
std.debug.assert(self.js_context == null);
const ModuleLoader = switch (@typeInfo(@TypeOf(module_loader))) {
@@ -402,30 +399,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
const global_template = js_global.getInstanceTemplate();
global_template.setInternalFieldCount(1);
// Configure the missing property callback on the global
// object.
if (global_callback != null) {
const configuration = v8.NamedPropertyHandlerConfiguration{
.getter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
const _isolate = info.getIsolate();
const v8_context = _isolate.getCurrentContext();
const js_context: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
const property = valueToString(js_context.call_arena, .{ .handle = c_name.? }, _isolate, v8_context) catch "???";
if (js_context.global_callback.?.missing(property, js_context)) {
return v8.Intercepted.Yes;
}
return v8.Intercepted.No;
}
}.callback,
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
};
global_template.setNamedProperty(configuration, null);
}
// All the FunctionTemplates that we created and setup in Env.init
// are now going to get associated with our global instance.
inline for (Types, 0..) |s, i| {
@@ -508,7 +481,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
.ptr = safe_module_loader,
.func = ModuleLoader.fetchModuleSource,
},
.global_callback = global_callback,
};
var js_context = &self.js_context.?;
@@ -634,15 +606,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// The key is the @intFromPtr of the Zig value
identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty,
// Some web APIs have to manage opaque values. Ideally, they use an
// Similar to the identity map, but used much less frequently. Some
// web APIs have to manage opaque values. Ideally, they use an
// JsObject, but the JsObject has no lifetime guarantee beyond the
// current call. They can call .persist() on their JsObject to get
// a `*PersistentObject()`. We need to track these to free them.
// This used to be a map and acted like identity_map; the key was
// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without
// a reliable way to know if an object has already been persisted,
// we now simply persist every time persist() is called.
js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty,
// The key is the @intFromPtr of the v8.Object.handle.
js_object_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty,
// When we need to load a resource (i.e. an external script), we call
// this function to get the source. This is always a reference to the
@@ -663,9 +633,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// necessary to lookup/store the dependent module in the module_cache.
module_identifier: std.AutoHashMapUnmanaged(u32, []const u8) = .empty,
// Global callback is called on missing property.
global_callback: ?GlobalMissingCallback = null,
const ModuleLoader = struct {
ptr: *anyopaque,
func: *const fn (ptr: *anyopaque, specifier: []const u8) anyerror!?[]const u8,
@@ -692,8 +659,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
}
}
for (self.js_object_list.items) |*p| {
p.deinit();
{
var it = self.js_object_map.valueIterator();
while (it.next()) |p| {
p.deinit();
}
}
{
@@ -749,9 +719,18 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
}
pub fn exec(self: *JsContext, src: []const u8, name: ?[]const u8) !Value {
const isolate = self.isolate;
const v8_context = self.v8_context;
const scr = try compileScript(self.isolate, v8_context, src, name);
var origin: ?v8.ScriptOrigin = null;
if (name) |n| {
const scr_name = v8.String.initUtf8(isolate, n);
origin = v8.ScriptOrigin.initDefault(scr_name.toValue());
}
const scr_js = v8.String.initUtf8(isolate, src);
const scr = v8.Script.compile(v8_context, scr_js, origin) catch {
return error.CompilationError;
};
const value = scr.run(v8_context) catch {
return error.ExecutionError;
@@ -761,13 +740,17 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
}
// compile and eval a JS module
// It returns null if the module is already compiled and in the cache.
// It returns a v8.Promise if the module must be evaluated.
pub fn module(self: *JsContext, src: []const u8, url: []const u8, cacheable: bool) !?v8.Promise {
// It doesn't wait for callbacks execution
pub fn module(self: *JsContext, src: []const u8, url: []const u8, cacheable: bool) !void {
if (!cacheable) {
return self.moduleNoCache(src, url);
}
const arena = self.context_arena;
if (cacheable and self.module_cache.contains(url)) {
return null;
const gop = try self.module_cache.getOrPut(arena, url);
if (gop.found_existing) {
return;
}
errdefer _ = self.module_cache.remove(url);
@@ -777,13 +760,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
errdefer _ = self.module_identifier.remove(m.getIdentityHash());
if (cacheable) {
try self.module_cache.putNoClobber(
arena,
owned_url,
PersistentModule.init(self.isolate, m),
);
}
gop.key_ptr.* = owned_url;
gop.value_ptr.* = PersistentModule.init(self.isolate, m);
// resolveModuleCallback loads module's dependencies.
const v8_context = self.v8_context;
@@ -791,12 +769,21 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
return error.ModuleInstantiationError;
}
const evaluated = try m.evaluate(v8_context);
// https://v8.github.io/api/head/classv8_1_1Module.html#a1f1758265a4082595757c3251bb40e0f
// Must be a promise that gets returned here.
std.debug.assert(evaluated.isPromise());
const promise = v8.Promise{ .handle = evaluated.handle };
return promise;
_ = try m.evaluate(v8_context);
}
fn moduleNoCache(self: *JsContext, src: []const u8, url: []const u8) !void {
const m = try compileModule(self.isolate, src, url);
const arena = self.context_arena;
const owned_url = try arena.dupe(u8, url);
try self.module_identifier.putNoClobber(arena, m.getIdentityHash(), owned_url);
const v8_context = self.v8_context;
if (try m.instantiate(v8_context, resolveModuleCallback) == false) {
return error.ModuleInstantiationError;
}
_ = try m.evaluate(v8_context);
}
// Wrap a v8.Exception
@@ -952,21 +939,9 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
}
},
.slice => {
var force_u8 = false;
var array_buffer: ?v8.ArrayBuffer = null;
if (js_value.isTypedArray()) {
const buffer_view = js_value.castTo(v8.ArrayBufferView);
array_buffer = buffer_view.getBuffer();
} else if (js_value.isArrayBufferView()) {
force_u8 = true;
const buffer_view = js_value.castTo(v8.ArrayBufferView);
array_buffer = buffer_view.getBuffer();
} else if (js_value.isArrayBuffer()) {
force_u8 = true;
array_buffer = js_value.castTo(v8.ArrayBuffer);
}
if (array_buffer) |buffer| {
const buffer = buffer_view.getBuffer();
const backing_store = v8.BackingStore.sharedPtrGet(&buffer.getBackingStore());
const data = backing_store.getData();
const byte_len = backing_store.getByteLength();
@@ -975,7 +950,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
u8 => {
// need this sentinel check to keep the compiler happy
if (ptr.sentinel() == null) {
if (force_u8 or js_value.isUint8Array() or js_value.isUint8ClampedArray()) {
if (js_value.isUint8Array() or js_value.isUint8ClampedArray()) {
const arr_ptr = @as([*]u8, @alignCast(@ptrCast(data)));
return arr_ptr[0..byte_len];
}
@@ -1170,13 +1145,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
};
}
pub fn createPromiseResolver(self: *JsContext) PromiseResolver {
return .{
.js_context = self,
.resolver = v8.PromiseResolver.init(self.v8_context),
};
}
// Probing is part of trying to map a JS value to a Zig union. There's
// a lot of ambiguity in this process, in part because some JS values
// can almost always be coerced. For example, anything can be coerced
@@ -1520,167 +1488,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
type_index = prototype_index;
}
}
pub fn dynamicModuleCallback(
v8_ctx: ?*const v8.c.Context,
host_defined_options: ?*const v8.c.Data,
resource_name: ?*const v8.c.Value,
v8_specifier: ?*const v8.c.String,
import_attrs: ?*const v8.c.FixedArray,
) callconv(.c) ?*v8.c.Promise {
_ = host_defined_options;
_ = import_attrs;
const ctx: v8.Context = .{ .handle = v8_ctx.? };
const context: *JsContext = @ptrFromInt(ctx.getEmbedderData(1).castTo(v8.BigInt).getUint64());
const iso = context.isolate;
const resolver = v8.PromiseResolver.init(context.v8_context);
const specifier: v8.String = .{ .handle = v8_specifier.? };
const specifier_str = jsStringToZig(context.context_arena, specifier, iso) catch {
const error_msg = v8.String.initUtf8(iso, "Failed to parse module specifier");
_ = resolver.reject(ctx, error_msg.toValue());
return @constCast(resolver.getPromise().handle);
};
const resource: v8.String = .{ .handle = resource_name.? };
const resource_str = jsStringToZig(context.context_arena, resource, iso) catch {
const error_msg = v8.String.initUtf8(iso, "Failed to parse module resource");
_ = resolver.reject(ctx, error_msg.toValue());
return @constCast(resolver.getPromise().handle);
};
const normalized_specifier = @import("../url.zig").stitch(
context.context_arena,
specifier_str,
resource_str,
.{ .alloc = .if_needed },
) catch unreachable;
log.debug(.js, "dynamic import", .{
.specifier = specifier_str,
.resource = resource_str,
.normalized_specifier = normalized_specifier,
});
_dynamicModuleCallback(context, normalized_specifier, &resolver) catch |err| {
log.err(.js, "dynamic module callback", .{
.err = err,
});
// Must be rejected at this point
// otherwise, we will just wait on a pending promise.
std.debug.assert(resolver.getPromise().getState() == .kRejected);
};
return @constCast(resolver.getPromise().handle);
}
fn _dynamicModuleCallback(
self: *JsContext,
specifier: []const u8,
resolver: *const v8.PromiseResolver,
) !void {
const iso = self.isolate;
const ctx = self.v8_context;
if (self.module_cache.get(specifier)) |cached_module| {
const new_module = cached_module.castToModule();
const status = new_module.getStatus();
switch (status) {
.kEvaluated, .kEvaluating => {
const namespace = new_module.getModuleNamespace();
log.info(.js, "dynamic import complete", .{
.specifier = specifier,
});
_ = resolver.resolve(ctx, namespace);
return;
},
else => {},
}
} else {
const module_loader = self.module_loader;
const source = module_loader.func(module_loader.ptr, specifier) catch {
const error_msg = v8.String.initUtf8(iso, "Failed to load module");
_ = resolver.reject(ctx, error_msg.toValue());
return;
} orelse {
const error_msg = v8.String.initUtf8(iso, "Module source not available");
_ = resolver.reject(ctx, error_msg.toValue());
return;
};
var try_catch: TryCatch = undefined;
try_catch.init(self);
defer try_catch.deinit();
const promise = self.module(source, specifier, true) catch {
log.err(.js, "module compilation failed", .{
.specifier = specifier,
.exception = try_catch.exception(self.call_arena) catch "unknown error",
.stack = try_catch.stack(self.call_arena) catch null,
.line = try_catch.sourceLineNumber() orelse 0,
});
const error_msg = if (try_catch.hasCaught()) blk: {
const exception_str = try_catch.exception(self.call_arena) catch "Evaluation error";
break :blk v8.String.initUtf8(iso, exception_str orelse "Evaluation error");
} else v8.String.initUtf8(iso, "Module evaluation failed");
_ = resolver.reject(ctx, error_msg.toValue());
return;
} orelse unreachable;
const new_module = self.module_cache.get(specifier).?.castToModule();
// This means we must wait for the evaluation.
const EvaluationData = struct {
specifier: []const u8,
module: v8.Persistent(v8.Module),
resolver: v8.Persistent(v8.PromiseResolver),
};
const ev_data = try self.context_arena.create(EvaluationData);
ev_data.* = .{
.specifier = specifier,
.module = v8.Persistent(v8.Module).init(iso, new_module),
.resolver = v8.Persistent(v8.PromiseResolver).init(iso, resolver.*),
};
const external = v8.External.init(iso, @ptrCast(ev_data));
const then_callback = v8.Function.initWithData(ctx, struct {
pub fn callback(info: ?*const v8.c.FunctionCallbackInfo) callconv(.c) void {
const cb_info = v8.FunctionCallbackInfo{ .handle = info.? };
const cb_isolate = cb_info.getIsolate();
const cb_context = cb_isolate.getCurrentContext();
const data: *EvaluationData = @ptrCast(@alignCast(cb_info.getExternalValue()));
const cb_module = data.module.castToModule();
const cb_resolver = data.resolver.castToPromiseResolver();
const namespace = cb_module.getModuleNamespace();
log.info(.js, "dynamic import complete", .{ .specifier = data.specifier });
_ = cb_resolver.resolve(cb_context, namespace);
}
}.callback, external);
const catch_callback = v8.Function.initWithData(ctx, struct {
pub fn callback(info: ?*const v8.c.FunctionCallbackInfo) callconv(.c) void {
const cb_info = v8.FunctionCallbackInfo{ .handle = info.? };
const cb_context = cb_info.getIsolate().getCurrentContext();
const data: *EvaluationData = @ptrCast(@alignCast(cb_info.getExternalValue()));
const cb_resolver = data.resolver.castToPromiseResolver();
log.err(.js, "dynamic import failed", .{ .specifier = data.specifier });
_ = cb_resolver.reject(cb_context, cb_info.getData());
}
}.callback, external);
_ = promise.thenAndCatch(ctx, then_callback, catch_callback) catch {
log.err(.js, "module evaluation is promise", .{
.specifier = specifier,
.line = try_catch.sourceLineNumber() orelse 0,
});
const error_msg = v8.String.initUtf8(iso, "Evaluation is a promise");
_ = resolver.reject(ctx, error_msg.toValue());
return;
};
}
}
};
pub const Function = struct {
@@ -1784,28 +1591,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
const js_this = try js_context.valueToExistingObject(this);
const aargs = if (comptime @typeInfo(@TypeOf(args)) == .null) struct {}{} else args;
const fields = @typeInfo(@TypeOf(aargs)).@"struct".fields;
var js_args: [fields.len]v8.Value = undefined;
inline for (fields, 0..) |f, i| {
js_args[i] = try js_context.zigValueToJs(@field(aargs, f.name));
}
const js_args: []const v8.Value = switch (@typeInfo(@TypeOf(aargs))) {
.@"struct" => |s| blk: {
const fields = s.fields;
var js_args: [fields.len]v8.Value = undefined;
inline for (fields, 0..) |f, i| {
js_args[i] = try js_context.zigValueToJs(@field(aargs, f.name));
}
const cargs: [fields.len]v8.Value = js_args;
break :blk &cargs;
},
.pointer => blk: {
var values = try js_context.call_arena.alloc(v8.Value, args.len);
for (args, 0..) |a, i| {
values[i] = try js_context.zigValueToJs(a);
}
break :blk values;
},
else => @compileError("JS Function called with invalid paremter type"),
};
const result = self.func.castToFunction().call(js_context.v8_context, js_this, js_args);
const result = self.func.castToFunction().call(js_context.v8_context, js_this, &js_args);
if (result == null) {
return error.JSExecCallback;
}
@@ -1901,13 +1693,16 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
pub fn persist(self: JsObject) !JsObject {
var js_context = self.js_context;
const js_obj = self.js_obj;
const handle = js_obj.handle;
const persisted = PersistentObject.init(js_context.isolate, js_obj);
try js_context.js_object_list.append(js_context.context_arena, persisted);
const gop = try js_context.js_object_map.getOrPut(js_context.context_arena, @intFromPtr(handle));
if (gop.found_existing == false) {
gop.value_ptr.* = PersistentObject.init(js_context.isolate, js_obj);
}
return .{
.js_context = js_context,
.js_obj = persisted.castToObject(),
.js_obj = gop.value_ptr.castToObject(),
};
}
@@ -2057,32 +1852,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
};
}
pub const PromiseResolver = struct {
js_context: *JsContext,
resolver: v8.PromiseResolver,
pub fn promise(self: PromiseResolver) Promise {
return .{
.promise = self.resolver.getPromise(),
};
}
pub fn resolve(self: PromiseResolver, value: anytype) !void {
const js_context = self.js_context;
const js_value = try js_context.zigValueToJs(value);
// resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.resolve(js_context.v8_context, js_value) orelse return;
if (!ok) {
return error.FailedToResolvePromise;
}
}
};
pub const Promise = struct {
promise: v8.Promise,
};
pub const Inspector = struct {
isolate: v8.Isolate,
inner: *v8.Inspector,
@@ -2238,25 +2007,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
};
}
fn compileScript(isolate: v8.Isolate, ctx: v8.Context, src: []const u8, name: ?[]const u8) !v8.Script {
// compile
const script_name = v8.String.initUtf8(isolate, name orelse "anonymous");
const script_source = v8.String.initUtf8(isolate, src);
const origin = v8.ScriptOrigin.initDefault(script_name.toValue());
var script_comp_source: v8.ScriptCompilerSource = undefined;
v8.ScriptCompilerSource.init(&script_comp_source, script_source, origin, null);
defer script_comp_source.deinit();
return v8.ScriptCompiler.compile(
ctx,
&script_comp_source,
.kNoCompileOptions,
.kNoCacheNoReason,
) catch return error.CompilationError;
}
fn compileModule(isolate: v8.Isolate, src: []const u8, name: []const u8) !v8.Module {
// compile
const script_name = v8.String.initUtf8(isolate, name);
@@ -2353,6 +2103,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
var caller = Caller(JsContext, State).init(info);
defer caller.deinit();
// See comment above. We generateConstructor on all types
// in order to create the FunctionTemplate, but there might
// not be an actual "constructor" function. So if someone
@@ -2360,7 +2111,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// a constructor function, we'll return an error.
if (@hasDecl(Struct, "constructor") == false) {
const iso = caller.isolate;
log.warn(.js, "Illegal constructor call", .{ .name = @typeName(Struct) });
const js_exception = iso.throwException(createException(iso, "Illegal Constructor"));
info.getReturnValue().set(js_exception);
return;
@@ -2515,6 +2265,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
fn generateNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
if (@hasDecl(Struct, "named_get") == false) {
if (comptime builtin.mode == .Debug) {
if (log.enabled(.unknown_prop, .debug)) {
generateDebugNamedIndexer(Struct, template_proto);
}
}
return;
}
@@ -2574,6 +2329,31 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
template_proto.setNamedProperty(configuration, null);
}
fn generateDebugNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
const configuration = v8.NamedPropertyHandlerConfiguration{
.getter = struct {
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
const isolate = info.getIsolate();
const v8_context = isolate.getCurrentContext();
const js_context: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
const property = valueToString(js_context.call_arena, .{ .handle = c_name.? }, isolate, v8_context) catch "???";
log.debug(.unknown_prop, "unkown property", .{ .@"struct" = @typeName(Struct), .property = property });
return v8.Intercepted.No;
}
}.callback,
// This is really cool. Without this, we'd intercept _all_ properties
// even those explicitly set. So, node.length for example would get routed
// to our `named_get`, rather than a `get_length`. This might be
// useful if we run into a type that we can't model properly in Zig.
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
};
template_proto.setNamedProperty(configuration, null);
}
fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void {
const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction");
@@ -2687,11 +2467,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
return value.js_obj.toValue();
}
if (T == Promise) {
// we're returning a v8.Promise
return value.promise.toObject().toValue();
}
if (@hasDecl(T, "_EXCEPTION_ID_KLUDGE")) {
return isolate.throwException(value.inner);
}
@@ -2802,35 +2577,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
self.callScopeEndFn(self.ptr);
}
};
// Callback called on global's property mssing.
// Return true to intercept the exectution or false to let the call
// continue the chain.
pub const GlobalMissingCallback = struct {
ptr: *anyopaque,
missingFn: *const fn (ptr: *anyopaque, name: []const u8, ctx: *JsContext) bool,
pub fn init(ptr: anytype) GlobalMissingCallback {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn missing(pointer: *anyopaque, name: []const u8, ctx: *JsContext) bool {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.pointer.child.missing(self, name, ctx);
}
};
return .{
.ptr = ptr,
.missingFn = gen.missing,
};
}
pub fn missing(self: GlobalMissingCallback, name: []const u8, ctx: *JsContext) bool {
return self.missingFn(self.ptr, name, ctx);
}
};
};
}

View File

@@ -336,8 +336,4 @@ test "JS: primitive types" {
.{ "p.returnFloat32()", "1.100000023841858,-200.03500366210938,0.0003000000142492354" },
.{ "p.returnFloat64()", "8881.22284,-4928.3838122,-0.00004" },
}, .{});
try runner.testCases(&.{
.{ "'foo\\\\:bar'", "foo\\:bar" },
}, .{});
}

View File

@@ -53,7 +53,6 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
state,
{},
true,
null,
);
return self;
}

View File

@@ -633,10 +633,6 @@ pub const Client = struct {
}
fn queueSend(self: *Self) void {
if (self.connected == false) {
return;
}
const node = self.send_queue.first orelse {
// no more messages to send;
return;

View File

@@ -474,12 +474,6 @@ pub const JsRunner = struct {
return err;
};
}
pub fn dispatchDOMContentLoaded(self: *JsRunner) !void {
const HTMLDocument = @import("browser/html/document.zig").HTMLDocument;
const html_doc = self.page.window.document;
try HTMLDocument.documentIsLoaded(html_doc, self.page);
}
};
const RunnerOpts = struct {

View File

@@ -100,72 +100,52 @@ pub const URL = struct {
/// For URLs without a path, it will add src as the path.
pub fn stitch(
allocator: Allocator,
path: []const u8,
src: []const u8,
base: []const u8,
opts: StitchOpts,
) ![]const u8 {
if (base.len == 0 or isComleteHTTPUrl(path)) {
if (base.len == 0 or isURL(src)) {
if (opts.alloc == .always) {
return allocator.dupe(u8, path);
return allocator.dupe(u8, src);
}
return path;
return src;
}
if (path.len == 0) {
var normalized_src = src;
while (std.mem.startsWith(u8, normalized_src, "./")) {
normalized_src = normalized_src[2..];
}
if (normalized_src.len == 0) {
if (opts.alloc == .always) {
return allocator.dupe(u8, base);
}
return base;
}
// Quick hack because domains have to be at least 3 characters.
// Given https://a.b this will point to 'a'
// Given http://a.b this will point '.'
// Either way, we just care about this value to find the start of the path
const protocol_end: usize = if (isComleteHTTPUrl(base)) 8 else 0;
if (path[0] == '/') {
const pos = std.mem.indexOfScalarPos(u8, base, protocol_end, '/') orelse base.len;
return std.fmt.allocPrint(allocator, "{s}{s}", .{ base[0..pos], path });
}
var normalized_base = base;
if (std.mem.lastIndexOfScalar(u8, base[protocol_end..], '/')) |pos| {
normalized_base = base[0 .. pos + protocol_end];
}
var out = try std.fmt.allocPrint(allocator, "{s}/{s}", .{
normalized_base,
path,
});
// Strip out ./ and ../. This is done in-place, because doing so can
// only ever make `out` smaller. After this, `out` cannot be freed by
// an allocator, which is ok, because we expect allocator to be an arena.
var in_i: usize = 0;
var out_i: usize = 0;
while (in_i < out.len) {
if (std.mem.startsWith(u8, out[in_i..], "./")) {
in_i += 2;
continue;
const protocol_end: usize = blk: {
if (std.mem.indexOf(u8, base, "://")) |protocol_index| {
break :blk protocol_index + 3;
} else {
break :blk 0;
}
if (std.mem.startsWith(u8, out[in_i..], "../")) {
std.debug.assert(out[out_i - 1] == '/');
};
out_i -= 2;
while (out_i > 1 and out[out_i - 1] != '/') {
out_i -= 1;
}
// <= to deal with the hack-ish protocol_end which will be off-by-one between http and https
if (out_i <= protocol_end) return error.InvalidURL;
in_i += 3;
continue;
if (normalized_src[0] == '/') {
if (std.mem.indexOfScalarPos(u8, base, protocol_end, '/')) |pos| {
return std.fmt.allocPrint(allocator, "{s}{s}", .{ base[0..pos], normalized_src });
}
out[out_i] = out[in_i];
in_i += 1;
out_i += 1;
// not sure what to do here...error? Just let it fallthrough for now.
}
return out[0..out_i];
if (std.mem.lastIndexOfScalar(u8, base[protocol_end..], '/')) |index| {
const last_slash_pos = index + protocol_end;
if (last_slash_pos == base.len - 1) {
return std.fmt.allocPrint(allocator, "{s}{s}", .{ base, normalized_src });
}
return std.fmt.allocPrint(allocator, "{s}/{s}", .{ base[0..last_slash_pos], normalized_src });
}
return std.fmt.allocPrint(allocator, "{s}/{s}", .{ base, normalized_src });
}
pub fn concatQueryString(arena: Allocator, url: []const u8, query_string: []const u8) ![]const u8 {
@@ -194,7 +174,7 @@ pub const URL = struct {
}
};
fn isComleteHTTPUrl(url: []const u8) bool {
fn isURL(url: []const u8) bool {
if (std.mem.startsWith(u8, url, "://")) {
return true;
}
@@ -215,17 +195,17 @@ fn isComleteHTTPUrl(url: []const u8) bool {
}
const testing = @import("testing.zig");
test "URL: isComleteHTTPUrl" {
try testing.expectEqual(true, isComleteHTTPUrl("://lightpanda.io"));
try testing.expectEqual(true, isComleteHTTPUrl("://lightpanda.io/about"));
try testing.expectEqual(true, isComleteHTTPUrl("http://lightpanda.io/about"));
try testing.expectEqual(true, isComleteHTTPUrl("HttP://lightpanda.io/about"));
try testing.expectEqual(true, isComleteHTTPUrl("httpS://lightpanda.io/about"));
try testing.expectEqual(true, isComleteHTTPUrl("HTTPs://lightpanda.io/about"));
test "URL: isURL" {
try testing.expectEqual(true, isURL("://lightpanda.io"));
try testing.expectEqual(true, isURL("://lightpanda.io/about"));
try testing.expectEqual(true, isURL("http://lightpanda.io/about"));
try testing.expectEqual(true, isURL("HttP://lightpanda.io/about"));
try testing.expectEqual(true, isURL("httpS://lightpanda.io/about"));
try testing.expectEqual(true, isURL("HTTPs://lightpanda.io/about"));
try testing.expectEqual(false, isComleteHTTPUrl("/lightpanda.io"));
try testing.expectEqual(false, isComleteHTTPUrl("../../about"));
try testing.expectEqual(false, isComleteHTTPUrl("about"));
try testing.expectEqual(false, isURL("/lightpanda.io"));
try testing.expectEqual(false, isURL("../../about"));
try testing.expectEqual(false, isURL("about"));
}
test "URL: resolve size" {
@@ -244,122 +224,93 @@ test "URL: resolve size" {
try std.testing.expectEqualStrings(out_url.raw[26..], &url_string);
}
test "URL: stitch" {
defer testing.reset();
test "URL: Stitching Base & Src URLs (Basic)" {
const allocator = testing.allocator;
const Case = struct {
base: []const u8,
path: []const u8,
expected: []const u8,
};
const base = "https://lightpanda.io/xyz/abc/123";
const src = "something.js";
const result = try URL.stitch(allocator, src, base, .{});
defer allocator.free(result);
try testing.expectString("https://lightpanda.io/xyz/abc/something.js", result);
}
const cases = [_]Case{
.{
.base = "https://lightpanda.io/xyz/abc/123",
.path = "something.js",
.expected = "https://lightpanda.io/xyz/abc/something.js",
},
.{
.base = "https://lightpanda.io/xyz/abc/123",
.path = "/something.js",
.expected = "https://lightpanda.io/something.js",
},
.{
.base = "https://lightpanda.io/",
.path = "something.js",
.expected = "https://lightpanda.io/something.js",
},
.{
.base = "https://lightpanda.io/",
.path = "/something.js",
.expected = "https://lightpanda.io/something.js",
},
.{
.base = "https://lightpanda.io",
.path = "something.js",
.expected = "https://lightpanda.io/something.js",
},
.{
.base = "https://lightpanda.io",
.path = "abc/something.js",
.expected = "https://lightpanda.io/abc/something.js",
},
.{
.base = "https://lightpanda.io/nested",
.path = "abc/something.js",
.expected = "https://lightpanda.io/abc/something.js",
},
.{
.base = "https://lightpanda.io/nested/",
.path = "abc/something.js",
.expected = "https://lightpanda.io/nested/abc/something.js",
},
.{
.base = "https://lightpanda.io/nested/",
.path = "/abc/something.js",
.expected = "https://lightpanda.io/abc/something.js",
},
.{
.base = "https://lightpanda.io/nested/",
.path = "http://www.github.com/lightpanda-io/",
.expected = "http://www.github.com/lightpanda-io/",
},
.{
.base = "https://lightpanda.io/nested/",
.path = "",
.expected = "https://lightpanda.io/nested/",
},
.{
.base = "https://lightpanda.io/abc/aaa",
.path = "./hello/./world",
.expected = "https://lightpanda.io/abc/hello/world",
},
.{
.base = "https://lightpanda.io/abc/aaa/",
.path = "../hello",
.expected = "https://lightpanda.io/abc/hello",
},
.{
.base = "https://lightpanda.io/abc/aaa",
.path = "../hello",
.expected = "https://lightpanda.io/hello",
},
.{
.base = "https://lightpanda.io/abc/aaa/",
.path = "./.././.././hello",
.expected = "https://lightpanda.io/hello",
},
.{
.base = "some/page",
.path = "hello",
.expected = "some/hello",
},
.{
.base = "some/page/",
.path = "hello",
.expected = "some/page/hello",
},
test "URL: Stitching Base & Src URLs (Just Ending Slash)" {
const allocator = testing.allocator;
.{
.base = "some/page/other",
.path = ".././hello",
.expected = "some/hello",
},
};
const base = "https://lightpanda.io/";
const src = "something.js";
const result = try URL.stitch(allocator, src, base, .{});
defer allocator.free(result);
try testing.expectString("https://lightpanda.io/something.js", result);
}
for (cases) |case| {
const result = try stitch(testing.arena_allocator, case.path, case.base, .{});
try testing.expectString(case.expected, result);
}
test "URL: Stitching Base & Src URLs with leading slash" {
const allocator = testing.allocator;
try testing.expectError(
error.InvalidURL,
stitch(testing.arena_allocator, "../hello", "https://lightpanda.io/", .{}),
);
try testing.expectError(
error.InvalidURL,
stitch(testing.arena_allocator, "../hello", "http://lightpanda.io/", .{}),
);
const base = "https://lightpanda.io/";
const src = "/something.js";
const result = try URL.stitch(allocator, src, base, .{});
defer allocator.free(result);
try testing.expectString("https://lightpanda.io/something.js", result);
}
test "URL: Stitching Base & Src URLs (No Ending Slash)" {
const allocator = testing.allocator;
const base = "https://lightpanda.io";
const src = "something.js";
const result = try URL.stitch(allocator, src, base, .{});
defer allocator.free(result);
try testing.expectString("https://lightpanda.io/something.js", result);
}
test "URL: Stitching Base with absolute src" {
const allocator = testing.allocator;
const base = "https://lightpanda.io/hello";
const src = "/abc/something.js";
const result = try URL.stitch(allocator, src, base, .{});
defer allocator.free(result);
try testing.expectString("https://lightpanda.io/abc/something.js", result);
}
test "URL: Stiching Base & Src URLs (Both Local)" {
const allocator = testing.allocator;
const base = "./abcdef/123.js";
const src = "something.js";
const result = try URL.stitch(allocator, src, base, .{});
defer allocator.free(result);
try testing.expectString("./abcdef/something.js", result);
}
test "URL: Stiching src as full path" {
const allocator = testing.allocator;
const base = "https://www.lightpanda.io/";
const src = "https://lightpanda.io/something.js";
const result = try URL.stitch(allocator, src, base, .{ .alloc = .if_needed });
try testing.expectString("https://lightpanda.io/something.js", result);
}
test "URL: Stitching Base & Src URLs (empty src)" {
const allocator = testing.allocator;
const base = "https://lightpanda.io/xyz/abc/123";
const src = "";
const result = try URL.stitch(allocator, src, base, .{});
defer allocator.free(result);
try testing.expectString("https://lightpanda.io/xyz/abc/123", result);
}
test "URL: Stitching dotslash" {
const allocator = testing.allocator;
const base = "https://lightpanda.io/hello/";
const src = "./something.js";
const result = try URL.stitch(allocator, src, base, .{});
defer allocator.free(result);
try testing.expectString("https://lightpanda.io/hello/something.js", result);
}
test "URL: concatQueryString" {