mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 07:33:16 +00:00
Compare commits
38 Commits
cbor
...
custom_ele
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7f209b70a | ||
|
|
9b35736be3 | ||
|
|
9f54cb35f4 | ||
|
|
329bffb127 | ||
|
|
e2542f41b5 | ||
|
|
efc7b9d4a5 | ||
|
|
72915760c4 | ||
|
|
e9d7a946c5 | ||
|
|
714e5e0456 | ||
|
|
26e8642aca | ||
|
|
68dfb4ee86 | ||
|
|
f1ff789334 | ||
|
|
1f45d5b8e4 | ||
|
|
c20052f314 | ||
|
|
c28d87d59c | ||
|
|
237ddcba9a | ||
|
|
eadb5b6461 | ||
|
|
faebabe3c7 | ||
|
|
02c510b07f | ||
|
|
63541970eb | ||
|
|
a8a5605fe1 | ||
|
|
0c0ddc10ee | ||
|
|
9bd5ff69ef | ||
|
|
eadf351e82 | ||
|
|
e3afa294af | ||
|
|
582894cdc3 | ||
|
|
2788c36ca6 | ||
|
|
872a9d393d | ||
|
|
b1ca242d89 | ||
|
|
97c769e805 | ||
|
|
0de33b36f8 | ||
|
|
cf39bdc7f7 | ||
|
|
34b49498c9 | ||
|
|
3a4bd00020 | ||
|
|
effd07d8c0 | ||
|
|
d9ce89ab31 | ||
|
|
5483c52227 | ||
|
|
2b48902f1b |
@@ -13,8 +13,8 @@
|
||||
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
|
||||
},
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/faab44996a5cb74c71592bda404208fde4bf2e63.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH6xWyAwB_NFICSO4Q3O-c7gDKnYiwky5FhQzTZMIr",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/1d25fcf3ced688adca3c7a95a138771e4ebba692.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH61eyAwDICIkLAkfQcxsX4TMCKY80QiSUgNBQqx-u",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
//.tigerbeetle_io = .{ .path = "../tigerbeetle-io" },
|
||||
|
||||
@@ -605,6 +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 => return .{ .pseudo_element = pseudo_class },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ pub const PseudoClass = enum {
|
||||
placeholder,
|
||||
selection,
|
||||
spelling_error,
|
||||
modal,
|
||||
|
||||
pub const Error = error{
|
||||
InvalidPseudoClass,
|
||||
@@ -154,6 +155,7 @@ pub const PseudoClass = enum {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ pub const CSSStyleDeclaration = struct {
|
||||
return self.order.items.len;
|
||||
}
|
||||
|
||||
pub fn get_parentRule() ?CSSRule {
|
||||
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ pub const CharacterData = struct {
|
||||
// netsurf's CharacterData (text, comment) doesn't implement the
|
||||
// dom_node_get_attributes and thus will crash if we try to call nodeIsEqualNode.
|
||||
pub fn _isEqualNode(self: *parser.CharacterData, other_node: *parser.Node) !bool {
|
||||
if (try parser.nodeType(@ptrCast(self)) != try parser.nodeType(other_node)) {
|
||||
if (try parser.nodeType(@alignCast(@ptrCast(self))) != try parser.nodeType(other_node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
@@ -120,9 +121,18 @@ pub const Document = struct {
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
|
||||
pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
|
||||
const CreateElementResult = union(enum) {
|
||||
element: ElementUnion,
|
||||
custom: Env.JsObject,
|
||||
};
|
||||
|
||||
pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !CreateElementResult {
|
||||
if (try page.window.custom_elements.newInstance(tag_name)) |ce| {
|
||||
return .{ .custom = ce };
|
||||
}
|
||||
|
||||
const e = try parser.documentCreateElement(self, tag_name);
|
||||
return try Element.toInterface(e);
|
||||
return .{ .element = try Element.toInterface(e) };
|
||||
}
|
||||
|
||||
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {
|
||||
@@ -243,17 +253,23 @@ pub const Document = struct {
|
||||
return try TreeWalker.init(root, what_to_show, filter);
|
||||
}
|
||||
|
||||
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
if (state.active_element) |ae| {
|
||||
return try Element.toInterface(ae);
|
||||
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
|
||||
if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| {
|
||||
if (state.active_element) |ae| {
|
||||
return ae;
|
||||
}
|
||||
}
|
||||
|
||||
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
||||
return try Element.toInterface(@ptrCast(body));
|
||||
return @alignCast(@ptrCast(body));
|
||||
}
|
||||
|
||||
return get_documentElement(self);
|
||||
return try parser.documentGetDocumentElement(self);
|
||||
}
|
||||
|
||||
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
|
||||
const ae = (try getActiveElement(self, page)) orelse return null;
|
||||
return try Element.toInterface(ae);
|
||||
}
|
||||
|
||||
// TODO: some elements can't be focused, like if they're disabled
|
||||
@@ -261,7 +277,7 @@ pub const Document = struct {
|
||||
// we could look for the "disabled" attribute, but that's only meaningful
|
||||
// on certain types, and libdom's vtable doesn't seem to expose this.
|
||||
pub fn setFocus(self: *parser.Document, e: *parser.ElementHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
state.active_element = @ptrCast(e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ pub const MutationObserver = struct {
|
||||
cbk: Env.Function,
|
||||
arena: Allocator,
|
||||
|
||||
// List of records which were observed. When the scopeEnds, we need to
|
||||
// List of records which were observed. When the call scope ends, we need to
|
||||
// execute our callback with it.
|
||||
observed: std.ArrayListUnmanaged(*MutationRecord),
|
||||
|
||||
|
||||
@@ -496,7 +496,7 @@ pub const Node = struct {
|
||||
fn toNode(self: NodeOrText, doc: *parser.Document) !*parser.Node {
|
||||
return switch (self) {
|
||||
.node => |n| n,
|
||||
.text => |txt| @ptrCast(try parser.documentCreateTextNode(doc, txt)),
|
||||
.text => |txt| @alignCast(@ptrCast(try parser.documentCreateTextNode(doc, txt))),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ const WebApis = struct {
|
||||
@import("xhr/xhr.zig").Interfaces,
|
||||
@import("xhr/form_data.zig").Interfaces,
|
||||
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
||||
@import("webcomponents/webcomponents.zig").Interfaces,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn get_readyState(self: *parser.DocumentHTML, page: *Page) ![]const u8 {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
return @tagName(state.ready_state);
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn documentIsLoaded(self: *parser.DocumentHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
state.ready_state = .interactive;
|
||||
|
||||
const evt = try parser.eventCreate();
|
||||
@@ -278,7 +278,7 @@ pub const HTMLDocument = struct {
|
||||
}
|
||||
|
||||
pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
state.ready_state = .complete;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// 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 generate = @import("../../runtime/generate.zig");
|
||||
@@ -112,6 +113,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;
|
||||
@@ -133,7 +138,7 @@ pub const HTMLElement = struct {
|
||||
try Node.removeChildren(n);
|
||||
|
||||
// attach the text node.
|
||||
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @ptrCast(t)));
|
||||
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @alignCast(@ptrCast(t))));
|
||||
}
|
||||
|
||||
pub fn _click(e: *parser.ElementHTML) !void {
|
||||
@@ -174,6 +179,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
|
||||
@@ -183,6 +192,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
|
||||
@@ -191,6 +204,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);
|
||||
}
|
||||
@@ -245,7 +262,7 @@ pub const HTMLAnchorElement = struct {
|
||||
}
|
||||
|
||||
inline fn url(self: *parser.Anchor, page: *Page) !URL {
|
||||
return URL.constructor(.{ .element = @ptrCast(self) }, null, page); // TODO inject base url
|
||||
return URL.constructor(.{ .element = @alignCast(@ptrCast(self)) }, null, page); // TODO inject base url
|
||||
}
|
||||
|
||||
// TODO return a disposable string
|
||||
@@ -428,144 +445,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 {
|
||||
@@ -573,6 +686,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);
|
||||
}
|
||||
@@ -613,6 +730,7 @@ pub const HTMLImageElement = struct {
|
||||
pub const Factory = struct {
|
||||
pub const js_name = "Image";
|
||||
pub const subtype = .node;
|
||||
|
||||
pub const js_legacy_factory = true;
|
||||
pub const prototype = *HTMLImageElement;
|
||||
|
||||
@@ -631,6 +749,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);
|
||||
}
|
||||
@@ -719,114 +841,190 @@ 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 {
|
||||
pub const Self = parser.Link;
|
||||
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 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
|
||||
@@ -835,6 +1033,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),
|
||||
@@ -945,22 +1147,22 @@ pub const HTMLScriptElement = struct {
|
||||
}
|
||||
|
||||
pub fn get_onload(self: *parser.Script, page: *Page) !?Env.Function {
|
||||
const state = page.getNodeState(@ptrCast(self)) orelse return null;
|
||||
const state = page.getNodeState(@alignCast(@ptrCast(self))) orelse return null;
|
||||
return state.onload;
|
||||
}
|
||||
|
||||
pub fn set_onload(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
state.onload = function;
|
||||
}
|
||||
|
||||
pub fn get_onerror(self: *parser.Script, page: *Page) !?Env.Function {
|
||||
const state = page.getNodeState(@ptrCast(self)) orelse return null;
|
||||
const state = page.getNodeState(@alignCast(@ptrCast(self))) orelse return null;
|
||||
return state.onerror;
|
||||
}
|
||||
|
||||
pub fn set_onerror(self: *parser.Script, function: ?Env.Function, page: *Page) !void {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(self));
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
||||
state.onerror = function;
|
||||
}
|
||||
};
|
||||
@@ -969,101 +1171,166 @@ 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 {
|
||||
pub const Self = parser.Style;
|
||||
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 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 {
|
||||
pub const Self = parser.Template;
|
||||
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 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 {
|
||||
const elem: *align(@alignOf(*parser.Element)) parser.Element = @alignCast(e);
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(elem)));
|
||||
|
||||
return switch (tag) {
|
||||
.abbr, .acronym, .address, .article, .aside, .b, .basefont, .bdi, .bdo, .bgsound, .big, .center, .cite, .code, .dd, .details, .dfn, .dt, .em, .figcaption, .figure, .footer, .header, .hgroup, .i, .isindex, .keygen, .kbd, .main, .mark, .marquee, .menu, .menuitem, .nav, .nobr, .noframes, .noscript, .rp, .rt, .ruby, .s, .samp, .section, .small, .spacer, .strike, .strong, .sub, .summary, .sup, .tt, .u, .wbr, ._var => .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(elem)) },
|
||||
.a => .{ .HTMLAnchorElement = @as(*parser.Anchor, @ptrCast(elem)) },
|
||||
@@ -1135,6 +1402,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, .{});
|
||||
@@ -1369,9 +1646,7 @@ test "Browser.HTML.HtmlInputElement.propeties.form" {
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let elem_input = document.querySelector('input')", null },
|
||||
}, .{});
|
||||
try runner.testCases(&.{.{ "elem_input.form", "[object HTMLFormElement]" }}, .{}); // Initial value
|
||||
try runner.testCases(&.{
|
||||
.{ "elem_input.form", "[object HTMLFormElement]" }, // Initial value
|
||||
.{ "elem_input.form = 'foo'", null },
|
||||
.{ "elem_input.form", "[object HTMLFormElement]" }, // Invalid
|
||||
}, .{});
|
||||
|
||||
@@ -56,7 +56,7 @@ pub const HTMLSelectElement = struct {
|
||||
}
|
||||
|
||||
pub fn get_selectedIndex(select: *parser.Select, page: *Page) !i32 {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(select));
|
||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(select)));
|
||||
const selected_index = try parser.selectGetSelectedIndex(select);
|
||||
|
||||
// See the explicit_index_set field documentation
|
||||
@@ -75,7 +75,7 @@ pub const HTMLSelectElement = struct {
|
||||
// Libdom's dom_html_select_select_set_selected_index will crash if index
|
||||
// is out of range, and it doesn't properly unset options
|
||||
pub fn set_selectedIndex(select: *parser.Select, index: i32, page: *Page) !void {
|
||||
var state = try page.getOrCreateNodeState(@ptrCast(select));
|
||||
var state = try page.getOrCreateNodeState(@alignCast(@ptrCast(select)));
|
||||
state.explicit_index_set = true;
|
||||
|
||||
const options = try parser.selectGetOptions(select);
|
||||
|
||||
@@ -33,6 +33,7 @@ const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
|
||||
const Performance = @import("performance.zig").Performance;
|
||||
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
const CustomElementRegistry = @import("../webcomponents/custom_element_registry.zig").CustomElementRegistry;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
|
||||
@@ -58,6 +59,7 @@ pub const Window = struct {
|
||||
console: Console = .{},
|
||||
navigator: Navigator = .{},
|
||||
performance: Performance,
|
||||
custom_elements: CustomElementRegistry = .{},
|
||||
|
||||
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
|
||||
var fbs = std.io.fixedBufferStream("");
|
||||
@@ -163,6 +165,10 @@ pub const Window = struct {
|
||||
return &self.performance;
|
||||
}
|
||||
|
||||
pub fn get_customElements(self: *Window) *CustomElementRegistry {
|
||||
return &self.custom_elements;
|
||||
}
|
||||
|
||||
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
|
||||
return self.createTimeout(cbk, 5, page, .{ .animation_frame = true });
|
||||
}
|
||||
@@ -337,20 +343,15 @@ test "Browser.HTML.Window" {
|
||||
// Note however that we in this test do not wait as the request is just send to the browser
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let start;
|
||||
\\ let start = 0;
|
||||
\\ function step(timestamp) {
|
||||
\\ if (start === undefined) {
|
||||
\\ start = timestamp;
|
||||
\\ }
|
||||
\\ const elapsed = timestamp - start;
|
||||
\\ if (elapsed < 2000) {
|
||||
\\ requestAnimationFrame(step);
|
||||
\\ }
|
||||
\\ start = timestamp;
|
||||
\\ }
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "requestAnimationFrame(step);", null }, // returned id is checked in the next test
|
||||
.{ " start > 0", "true" },
|
||||
}, .{});
|
||||
|
||||
// cancelAnimationFrame should be able to cancel a request with the given id
|
||||
|
||||
@@ -25,7 +25,10 @@ const c = @cImport({
|
||||
@cInclude("events/event_target.h");
|
||||
@cInclude("events/event.h");
|
||||
@cInclude("events/mouse_event.h");
|
||||
@cInclude("events/keyboard_event.h");
|
||||
@cInclude("utils/validate.h");
|
||||
@cInclude("html/html_element.h");
|
||||
@cInclude("html/html_document.h");
|
||||
});
|
||||
|
||||
const mimalloc = @import("mimalloc.zig");
|
||||
@@ -550,7 +553,7 @@ pub fn mutationEventRelatedNode(evt: *MutationEvent) !?*Node {
|
||||
const err = c._dom_mutation_event_get_related_node(evt, &n);
|
||||
try DOMErr(err);
|
||||
if (n == null) return null;
|
||||
return @as(*Node, @ptrCast(n));
|
||||
return @as(*Node, @alignCast(@ptrCast(n)));
|
||||
}
|
||||
|
||||
// EventListener
|
||||
@@ -565,7 +568,7 @@ fn eventListenerGetData(lst: *EventListener) ?*anyopaque {
|
||||
pub const EventTarget = c.dom_event_target;
|
||||
|
||||
pub fn eventTargetToNode(et: *EventTarget) *Node {
|
||||
return @as(*Node, @ptrCast(et));
|
||||
return @as(*Node, @alignCast(@ptrCast(et)));
|
||||
}
|
||||
|
||||
fn eventTargetVtable(et: *EventTarget) c.dom_event_target_vtable {
|
||||
@@ -862,6 +865,59 @@ pub fn mouseEventDefaultPrevented(evt: *MouseEvent) !bool {
|
||||
return eventDefaultPrevented(@ptrCast(evt));
|
||||
}
|
||||
|
||||
// KeyboardEvent
|
||||
|
||||
pub const KeyboardEvent = c.dom_keyboard_event;
|
||||
|
||||
pub fn keyboardEventCreate() !*KeyboardEvent {
|
||||
var evt: ?*KeyboardEvent = undefined;
|
||||
const err = c._dom_keyboard_event_create(&evt);
|
||||
try DOMErr(err);
|
||||
return evt.?;
|
||||
}
|
||||
|
||||
pub fn keyboardEventDestroy(evt: *KeyboardEvent) void {
|
||||
c._dom_keyboard_event_destroy(evt);
|
||||
}
|
||||
|
||||
const KeyboardEventOpts = struct {
|
||||
key: []const u8,
|
||||
code: []const u8,
|
||||
bubbles: bool = false,
|
||||
cancelable: bool = false,
|
||||
ctrl: bool = false,
|
||||
alt: bool = false,
|
||||
shift: bool = false,
|
||||
meta: bool = false,
|
||||
};
|
||||
|
||||
pub fn keyboardEventInit(evt: *KeyboardEvent, typ: []const u8, opts: KeyboardEventOpts) !void {
|
||||
const s = try strFromData(typ);
|
||||
const err = c._dom_keyboard_event_init(
|
||||
evt,
|
||||
s,
|
||||
opts.bubbles,
|
||||
opts.cancelable,
|
||||
null, // dom_abstract_view* ?
|
||||
try strFromData(opts.key),
|
||||
try strFromData(opts.code),
|
||||
0, // location 0 == standard
|
||||
opts.ctrl,
|
||||
opts.shift,
|
||||
opts.alt,
|
||||
opts.meta,
|
||||
false, // repease
|
||||
false, // is_composiom
|
||||
);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn keyboardEventGetKey(evt: *KeyboardEvent) ![]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
_ = c._dom_keyboard_event_get_key(evt, &s);
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
// NodeType
|
||||
|
||||
pub const NodeType = enum(u4) {
|
||||
@@ -894,7 +950,7 @@ pub fn nodeListItem(nodeList: *NodeList, index: u32) !?*Node {
|
||||
const err = c._dom_nodelist_item(nodeList, index, &n);
|
||||
try DOMErr(err);
|
||||
if (n == null) return null;
|
||||
return @as(*Node, @ptrCast(n));
|
||||
return @as(*Node, @alignCast(@ptrCast(n)));
|
||||
}
|
||||
|
||||
// NodeExternal is the libdom public representation of a Node.
|
||||
@@ -1323,7 +1379,7 @@ fn characterDataVtable(data: *CharacterData) c.dom_characterdata_vtable {
|
||||
}
|
||||
|
||||
pub inline fn characterDataToNode(cdata: *CharacterData) *Node {
|
||||
return @as(*Node, @ptrCast(cdata));
|
||||
return @as(*Node, @alignCast(@ptrCast(cdata)));
|
||||
}
|
||||
|
||||
pub fn characterDataData(cdata: *CharacterData) ![]const u8 {
|
||||
@@ -1408,7 +1464,7 @@ pub const ProcessingInstruction = c.dom_processing_instruction;
|
||||
|
||||
// processingInstructionToNode is an helper to convert an ProcessingInstruction to a node.
|
||||
pub inline fn processingInstructionToNode(pi: *ProcessingInstruction) *Node {
|
||||
return @as(*Node, @ptrCast(pi));
|
||||
return @as(*Node, @alignCast(@ptrCast(pi)));
|
||||
}
|
||||
|
||||
pub fn processInstructionCopy(pi: *ProcessingInstruction) !*ProcessingInstruction {
|
||||
@@ -1463,7 +1519,7 @@ pub fn attributeGetOwnerElement(a: *Attribute) !?*Element {
|
||||
|
||||
// attributeToNode is an helper to convert an attribute to a node.
|
||||
pub inline fn attributeToNode(a: *Attribute) *Node {
|
||||
return @as(*Node, @ptrCast(a));
|
||||
return @as(*Node, @alignCast(@ptrCast(a)));
|
||||
}
|
||||
|
||||
// Element
|
||||
@@ -1601,7 +1657,7 @@ pub fn elementHasClass(elem: *Element, class: []const u8) !bool {
|
||||
|
||||
// elementToNode is an helper to convert an element to a node.
|
||||
pub inline fn elementToNode(e: *Element) *Node {
|
||||
return @as(*Node, @ptrCast(e));
|
||||
return @as(*Node, @alignCast(@ptrCast(e)));
|
||||
}
|
||||
|
||||
// TokenList
|
||||
@@ -1685,14 +1741,14 @@ pub fn elementHTMLGetTagType(elem_html: *ElementHTML) !Tag {
|
||||
|
||||
// scriptToElt is an helper to convert an script to an element.
|
||||
pub inline fn scriptToElt(s: *Script) *Element {
|
||||
return @as(*Element, @ptrCast(s));
|
||||
return @as(*Element, @alignCast(@ptrCast(s)));
|
||||
}
|
||||
|
||||
// HTMLAnchorElement
|
||||
|
||||
// anchorToNode is an helper to convert an anchor to a node.
|
||||
pub inline fn anchorToNode(a: *Anchor) *Node {
|
||||
return @as(*Node, @ptrCast(a));
|
||||
return @as(*Node, @alignCast(@ptrCast(a)));
|
||||
}
|
||||
|
||||
pub fn anchorGetTarget(a: *Anchor) ![]const u8 {
|
||||
@@ -1837,7 +1893,7 @@ pub const OptionCollection = c.dom_html_options_collection;
|
||||
pub const DocumentFragment = c.dom_document_fragment;
|
||||
|
||||
pub inline fn documentFragmentToNode(doc: *DocumentFragment) *Node {
|
||||
return @as(*Node, @ptrCast(doc));
|
||||
return @as(*Node, @alignCast(@ptrCast(doc)));
|
||||
}
|
||||
|
||||
pub fn documentFragmentBodyChildren(doc: *DocumentFragment) !?*NodeList {
|
||||
@@ -1947,7 +2003,7 @@ pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8) !*Document
|
||||
if (title) |t| {
|
||||
const htitle = try documentCreateElement(doc, "title");
|
||||
const txt = try documentCreateTextNode(doc, t);
|
||||
_ = try nodeAppendChild(elementToNode(htitle), @as(*Node, @ptrCast(txt)));
|
||||
_ = try nodeAppendChild(elementToNode(htitle), @as(*Node, @alignCast(@ptrCast(txt))));
|
||||
_ = try nodeAppendChild(elementToNode(head), elementToNode(htitle));
|
||||
}
|
||||
|
||||
@@ -1965,7 +2021,7 @@ fn documentVtable(doc: *Document) c.dom_document_vtable {
|
||||
}
|
||||
|
||||
pub inline fn documentToNode(doc: *Document) *Node {
|
||||
return @as(*Node, @ptrCast(doc));
|
||||
return @as(*Node, @alignCast(@ptrCast(doc)));
|
||||
}
|
||||
|
||||
pub inline fn documentGetElementById(doc: *Document, id: []const u8) !?*Element {
|
||||
@@ -2103,7 +2159,7 @@ pub inline fn documentImportNode(doc: *Document, node: *Node, deep: bool) !*Node
|
||||
const nodeext = toNodeExternal(Node, node);
|
||||
const err = documentVtable(doc).dom_document_import_node.?(doc, nodeext, deep, &res);
|
||||
try DOMErr(err);
|
||||
return @as(*Node, @ptrCast(res));
|
||||
return @as(*Node, @alignCast(@ptrCast(res)));
|
||||
}
|
||||
|
||||
pub inline fn documentAdoptNode(doc: *Document, node: *Node) !*Node {
|
||||
@@ -2111,7 +2167,7 @@ pub inline fn documentAdoptNode(doc: *Document, node: *Node) !*Node {
|
||||
const nodeext = toNodeExternal(Node, node);
|
||||
const err = documentVtable(doc).dom_document_adopt_node.?(doc, nodeext, &res);
|
||||
try DOMErr(err);
|
||||
return @as(*Node, @ptrCast(res));
|
||||
return @as(*Node, @alignCast(@ptrCast(res)));
|
||||
}
|
||||
|
||||
pub inline fn documentCreateAttribute(doc: *Document, name: []const u8) !*Attribute {
|
||||
@@ -2133,12 +2189,12 @@ pub inline fn documentCreateAttributeNS(doc: *Document, ns: []const u8, qname: [
|
||||
return attr.?;
|
||||
}
|
||||
|
||||
pub fn documentSetScriptAddedCallback(
|
||||
pub fn documentSetElementAddedCallback(
|
||||
doc: *Document,
|
||||
ctx: *anyopaque,
|
||||
callback: c.dom_script_added_callback,
|
||||
callback: c.dom_element_added_callback,
|
||||
) void {
|
||||
c._dom_document_set_script_added_callback(doc, ctx, callback);
|
||||
c._dom_document_set_element_added_callback(doc, ctx, callback);
|
||||
}
|
||||
|
||||
// DocumentHTML
|
||||
@@ -2146,7 +2202,7 @@ pub const DocumentHTML = c.dom_html_document;
|
||||
|
||||
// documentHTMLToNode is an helper to convert a documentHTML to an node.
|
||||
pub inline fn documentHTMLToNode(doc: *DocumentHTML) *Node {
|
||||
return @as(*Node, @ptrCast(doc));
|
||||
return @as(*Node, @alignCast(@ptrCast(doc)));
|
||||
}
|
||||
|
||||
fn documentHTMLVtable(doc_html: *DocumentHTML) c.dom_html_document_vtable {
|
||||
@@ -2291,7 +2347,7 @@ pub inline fn documentHTMLBody(doc_html: *DocumentHTML) !?*Body {
|
||||
}
|
||||
|
||||
pub inline fn bodyToElement(body: *Body) *Element {
|
||||
return @as(*Element, @ptrCast(body));
|
||||
return @as(*Element, @alignCast(@ptrCast(body)));
|
||||
}
|
||||
|
||||
pub inline fn documentHTMLSetBody(doc_html: *DocumentHTML, elt: ?*ElementHTML) !void {
|
||||
@@ -2330,7 +2386,7 @@ pub inline fn documentHTMLSetTitle(doc: *DocumentHTML, v: []const u8) !void {
|
||||
|
||||
pub fn documentHTMLSetCurrentScript(doc: *DocumentHTML, script: ?*Script) !void {
|
||||
var s: ?*ElementHTML = null;
|
||||
if (script != null) s = @ptrCast(script.?);
|
||||
if (script != null) s = @alignCast(@ptrCast(script.?));
|
||||
const err = documentHTMLVtable(doc).set_current_script.?(doc, s);
|
||||
try DOMErr(err);
|
||||
}
|
||||
@@ -2391,6 +2447,11 @@ pub fn textareaGetValue(textarea: *TextArea) ![]const u8 {
|
||||
return strToData(s);
|
||||
}
|
||||
|
||||
pub fn textareaSetValue(textarea: *TextArea, value: []const u8) !void {
|
||||
const err = c.dom_html_text_area_element_set_value(textarea, try strFromData(value));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
// Select
|
||||
pub fn selectGetOptions(select: *Select) !*OptionCollection {
|
||||
var collection: ?*OptionCollection = null;
|
||||
@@ -2759,7 +2820,7 @@ pub fn inputSetType(input: *Input, type_: []const u8) !void {
|
||||
}
|
||||
}
|
||||
const new_type = if (found) type_ else "text";
|
||||
try elementSetAttribute(@ptrCast(input), "type", new_type);
|
||||
try elementSetAttribute(@alignCast(@ptrCast(input)), "type", new_type);
|
||||
}
|
||||
|
||||
pub fn inputGetValue(input: *Input) ![]const u8 {
|
||||
@@ -2773,3 +2834,11 @@ pub fn inputSetValue(input: *Input, value: []const u8) !void {
|
||||
const err = c.dom_html_input_element_set_value(input, try strFromData(value));
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn buttonGetType(button: *Button) ![]const u8 {
|
||||
var s_: ?*String = null;
|
||||
const err = c.dom_html_button_element_get_type(button, &s_);
|
||||
try DOMErr(err);
|
||||
const s = s_ orelse return "button";
|
||||
return strToData(s);
|
||||
}
|
||||
|
||||
@@ -80,11 +80,12 @@ pub const Page = struct {
|
||||
|
||||
microtask_node: Loop.CallbackNode,
|
||||
|
||||
keydown_event_node: parser.EventNode,
|
||||
window_clicked_event_node: parser.EventNode,
|
||||
|
||||
// Our JavaScript context for this specific page. This is what we use to
|
||||
// execute any JavaScript
|
||||
scope: *Env.Scope,
|
||||
main_context: *Env.JsContext,
|
||||
|
||||
// List of modules currently fetched/loaded.
|
||||
module_map: std.StringHashMapUnmanaged([]const u8),
|
||||
@@ -112,17 +113,18 @@ pub const Page = struct {
|
||||
.state_pool = &browser.state_pool,
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
.microtask_node = .{ .func = microtaskCallback },
|
||||
.keydown_event_node = .{ .func = keydownCallback },
|
||||
.window_clicked_event_node = .{ .func = windowClicked },
|
||||
.request_factory = browser.http_client.requestFactory(.{
|
||||
.notification = browser.notification,
|
||||
}),
|
||||
.scope = undefined,
|
||||
.main_context = undefined,
|
||||
.module_map = .empty,
|
||||
};
|
||||
self.scope = try session.executor.startScope(&self.window, self, self, true);
|
||||
self.main_context = try session.executor.createJsContext(&self.window, self, self, true);
|
||||
|
||||
// load polyfills
|
||||
try polyfill.load(self.arena, self.scope);
|
||||
try polyfill.load(self.arena, self.main_context);
|
||||
|
||||
_ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node);
|
||||
}
|
||||
@@ -164,7 +166,7 @@ pub const Page = struct {
|
||||
|
||||
pub fn wait(self: *Page) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(self.scope);
|
||||
try_catch.init(self.main_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
try self.session.browser.app.loop.run();
|
||||
@@ -190,7 +192,12 @@ pub const Page = struct {
|
||||
const session = self.session;
|
||||
const notification = session.browser.notification;
|
||||
|
||||
log.debug(.http, "navigate", .{ .url = request_url, .reason = opts.reason });
|
||||
log.debug(.http, "navigate", .{
|
||||
.url = request_url,
|
||||
.method = opts.method,
|
||||
.reason = opts.reason,
|
||||
.body = opts.body != null,
|
||||
});
|
||||
|
||||
// if the url is about:blank, nothing to do.
|
||||
if (std.mem.eql(u8, "about:blank", request_url.raw)) {
|
||||
@@ -247,6 +254,8 @@ pub const Page = struct {
|
||||
.content_type = content_type,
|
||||
.charset = mime.charset,
|
||||
.url = request_url,
|
||||
.method = opts.method,
|
||||
.reason = opts.reason,
|
||||
});
|
||||
|
||||
if (!mime.isHTML()) {
|
||||
@@ -288,16 +297,17 @@ pub const Page = struct {
|
||||
self.window.setStorageShelf(
|
||||
try self.session.storage_shed.getOrPut(try self.origin(self.arena)),
|
||||
);
|
||||
|
||||
// we want to be notified of any dynamically added script tags
|
||||
// so that we can load the script. Or dynamically added custom elements
|
||||
// for their lifecycle callbacks.
|
||||
parser.documentSetElementAddedCallback(doc, self, elementAddedCallback);
|
||||
}
|
||||
|
||||
fn processHTMLDoc(self: *Page) !void {
|
||||
const html_doc = self.window.document;
|
||||
const doc = parser.documentHTMLToDocument(html_doc);
|
||||
|
||||
// we want to be notified of any dynamically added script tags
|
||||
// so that we can load the script
|
||||
parser.documentSetScriptAddedCallback(doc, self, scriptAddedCallback);
|
||||
|
||||
const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError;
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Element, document_element),
|
||||
@@ -305,6 +315,12 @@ pub const Page = struct {
|
||||
&self.window_clicked_event_node,
|
||||
false,
|
||||
);
|
||||
_ = try parser.eventTargetAddEventListener(
|
||||
parser.toEventTarget(parser.Element, document_element),
|
||||
"keydown",
|
||||
&self.keydown_event_node,
|
||||
false,
|
||||
);
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
|
||||
@@ -337,8 +353,23 @@ pub const Page = struct {
|
||||
continue;
|
||||
}
|
||||
|
||||
const e = parser.nodeToElement(next.?);
|
||||
const current = next.?;
|
||||
|
||||
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;
|
||||
@@ -373,11 +404,15 @@ pub const Page = struct {
|
||||
// > immediately before the browser continues to parse the
|
||||
// > page.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
|
||||
self.evalScript(&script);
|
||||
if (self.evalScript(&script) == false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (defer_scripts.items) |*script| {
|
||||
self.evalScript(script);
|
||||
if (self.evalScript(script) == false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// dispatch DOMContentLoaded before the transition to "complete",
|
||||
// at the point where all subresources apart from async script elements
|
||||
@@ -387,7 +422,9 @@ pub const Page = struct {
|
||||
|
||||
// eval async scripts.
|
||||
for (async_scripts.items) |*script| {
|
||||
self.evalScript(script);
|
||||
if (self.evalScript(script) == false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try HTMLDocument.documentIsComplete(html_doc, self);
|
||||
@@ -404,10 +441,13 @@ pub const Page = struct {
|
||||
);
|
||||
}
|
||||
|
||||
fn evalScript(self: *Page, script: *const Script) void {
|
||||
self.tryEvalScript(script) catch |err| {
|
||||
log.err(.js, "eval script error", .{ .err = err, .src = script.src });
|
||||
fn evalScript(self: *Page, script: *const Script) bool {
|
||||
self.tryEvalScript(script) catch |err| switch (err) {
|
||||
error.JsErr => {}, // already been logged with detail
|
||||
error.Terminated => return false,
|
||||
else => log.err(.js, "eval script error", .{ .err = err, .src = script.src }),
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
// evalScript evaluates the src in priority.
|
||||
@@ -421,29 +461,26 @@ pub const Page = struct {
|
||||
log.err(.browser, "clear document script", .{ .err = err });
|
||||
};
|
||||
|
||||
const src = script.src orelse {
|
||||
var script_source: ?[]const u8 = null;
|
||||
if (script.src) |src| {
|
||||
self.current_script = script;
|
||||
defer self.current_script = null;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
||||
script_source = (try self.fetchData(src, null)) orelse {
|
||||
// TODO If el's result is null, then fire an event named error at
|
||||
// el, and return
|
||||
return;
|
||||
};
|
||||
} else {
|
||||
// source is inline
|
||||
// TODO handle charset attribute
|
||||
if (try parser.nodeTextContent(parser.elementToNode(script.element))) |text| {
|
||||
try script.eval(self, text);
|
||||
}
|
||||
return;
|
||||
};
|
||||
script_source = try parser.nodeTextContent(parser.elementToNode(script.element));
|
||||
}
|
||||
|
||||
self.current_script = script;
|
||||
defer self.current_script = null;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
|
||||
const body = (try self.fetchData(src, null)) orelse {
|
||||
// TODO If el's result is null, then fire an event named error at
|
||||
// el, and return
|
||||
return;
|
||||
};
|
||||
|
||||
script.eval(self, body) catch |err| switch (err) {
|
||||
error.JsErr => {}, // nothing to do here.
|
||||
else => return err,
|
||||
};
|
||||
if (script_source) |ss| {
|
||||
try script.eval(self, ss);
|
||||
}
|
||||
|
||||
// TODO If el's from an external file is true, then fire an event
|
||||
// named load at el.
|
||||
@@ -572,14 +609,14 @@ pub const Page = struct {
|
||||
},
|
||||
.input => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
const input_type = (try parser.elementGetAttribute(element, "type")) orelse return;
|
||||
const input_type = try parser.inputGetType(@ptrCast(element));
|
||||
if (std.ascii.eqlIgnoreCase(input_type, "submit")) {
|
||||
return self.elementSubmitForm(element);
|
||||
}
|
||||
},
|
||||
.button => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
const button_type = (try parser.elementGetAttribute(element, "type")) orelse return;
|
||||
const button_type = try parser.buttonGetType(@ptrCast(element));
|
||||
if (std.ascii.eqlIgnoreCase(button_type, "submit")) {
|
||||
return self.elementSubmitForm(element);
|
||||
}
|
||||
@@ -593,18 +630,111 @@ pub const Page = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub const KeyboardEvent = struct {
|
||||
type: Type,
|
||||
key: []const u8,
|
||||
code: []const u8,
|
||||
alt: bool,
|
||||
ctrl: bool,
|
||||
meta: bool,
|
||||
shift: bool,
|
||||
|
||||
const Type = enum {
|
||||
keydown,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn keyboardEvent(self: *Page, kbe: KeyboardEvent) !void {
|
||||
if (kbe.type != .keydown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Document = @import("dom/document.zig").Document;
|
||||
const element = (try Document.getActiveElement(@ptrCast(self.window.document), self)) orelse return;
|
||||
|
||||
const event = try parser.keyboardEventCreate();
|
||||
defer parser.keyboardEventDestroy(event);
|
||||
try parser.keyboardEventInit(event, "keydown", .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.key = kbe.key,
|
||||
.code = kbe.code,
|
||||
.alt = kbe.alt,
|
||||
.ctrl = kbe.ctrl,
|
||||
.meta = kbe.meta,
|
||||
.shift = kbe.shift,
|
||||
});
|
||||
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
|
||||
}
|
||||
|
||||
fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void {
|
||||
const self: *Page = @fieldParentPtr("keydown_event_node", node);
|
||||
self._keydownCallback(event) catch |err| {
|
||||
log.err(.browser, "keydown handler error", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
fn _keydownCallback(self: *Page, event: *parser.Event) !void {
|
||||
const target = (try parser.eventTarget(event)) orelse return;
|
||||
const node = parser.eventTargetToNode(target);
|
||||
const tag = (try parser.nodeHTMLGetTagType(node)) orelse return;
|
||||
|
||||
const kbe: *parser.KeyboardEvent = @ptrCast(event);
|
||||
var new_key = try parser.keyboardEventGetKey(kbe);
|
||||
if (std.mem.eql(u8, new_key, "Dead")) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (tag) {
|
||||
.input => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
const input_type = try parser.inputGetType(@ptrCast(element));
|
||||
if (std.mem.eql(u8, input_type, "text")) {
|
||||
if (std.mem.eql(u8, new_key, "Enter")) {
|
||||
const form = (try self.formForElement(element)) orelse return;
|
||||
return self.submitForm(@ptrCast(form), null);
|
||||
}
|
||||
|
||||
const value = try parser.inputGetValue(@ptrCast(element));
|
||||
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
|
||||
try parser.inputSetValue(@ptrCast(element), new_value);
|
||||
}
|
||||
},
|
||||
.textarea => {
|
||||
const value = try parser.textareaGetValue(@ptrCast(node));
|
||||
if (std.mem.eql(u8, new_key, "Enter")) {
|
||||
new_key = "\n";
|
||||
}
|
||||
const new_value = try std.mem.concat(self.arena, u8, &.{ value, new_key });
|
||||
try parser.textareaSetValue(@ptrCast(node), new_value);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
// We cannot navigate immediately as navigating will delete the DOM tree,
|
||||
// which holds this event's node.
|
||||
// As such we schedule the function to be called as soon as possible.
|
||||
// The page.arena is safe to use here, but the transfer_arena exists
|
||||
// specifically for this type of lifetime.
|
||||
pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts) !void {
|
||||
log.debug(.browser, "delayed navigation", .{
|
||||
.url = url,
|
||||
.reason = opts.reason,
|
||||
});
|
||||
self.delayed_navigation = true;
|
||||
const arena = self.session.transfer_arena;
|
||||
|
||||
const session = self.session;
|
||||
const arena = session.transfer_arena;
|
||||
const navi = try arena.create(DelayedNavigation);
|
||||
navi.* = .{
|
||||
.opts = opts,
|
||||
.session = self.session,
|
||||
.url = try arena.dupe(u8, url),
|
||||
.session = session,
|
||||
.url = try self.url.resolve(arena, url),
|
||||
};
|
||||
|
||||
// In v8, this throws an exception which JS code cannot catch.
|
||||
session.executor.terminateExecution();
|
||||
_ = try self.loop.timeout(0, &navi.navigate_node);
|
||||
}
|
||||
|
||||
@@ -633,13 +763,13 @@ pub const Page = struct {
|
||||
const transfer_arena = self.session.transfer_arena;
|
||||
var form_data = try FormData.fromForm(form, submitter, self);
|
||||
|
||||
const encoding = try parser.elementGetAttribute(@ptrCast(form), "enctype");
|
||||
const encoding = try parser.elementGetAttribute(@alignCast(@ptrCast(form)), "enctype");
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
try form_data.write(encoding, buf.writer(transfer_arena));
|
||||
|
||||
const method = try parser.elementGetAttribute(@ptrCast(form), "method") orelse "";
|
||||
var action = try parser.elementGetAttribute(@ptrCast(form), "action") orelse self.url.raw;
|
||||
const method = try parser.elementGetAttribute(@alignCast(@ptrCast(form)), "method") orelse "";
|
||||
var action = try parser.elementGetAttribute(@alignCast(@ptrCast(form)), "action") orelse self.url.raw;
|
||||
|
||||
var opts = NavigateOpts{
|
||||
.reason = .form,
|
||||
@@ -650,7 +780,6 @@ pub const Page = struct {
|
||||
} else {
|
||||
action = try URL.concatQueryString(transfer_arena, action, buf.items);
|
||||
}
|
||||
|
||||
try self.navigateFromWebAPI(action, opts);
|
||||
}
|
||||
|
||||
@@ -685,22 +814,126 @@ pub const Page = struct {
|
||||
|
||||
pub fn stackTrace(self: *Page) !?[]const u8 {
|
||||
if (comptime builtin.mode == .Debug) {
|
||||
return self.scope.stackTrace();
|
||||
return self.main_context.stackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn elementAdded(self: *Page, element: *parser.Element) !void {
|
||||
if (self.delayed_navigation) {
|
||||
// if we're planning on navigating to another page, we can skip whatever
|
||||
// this is.
|
||||
return;
|
||||
}
|
||||
|
||||
switch (try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(element)))) {
|
||||
.script => {
|
||||
var script = Script.init(element, self) catch |err| {
|
||||
log.warn(.browser, "script added", .{ .err = err });
|
||||
return;
|
||||
} orelse return;
|
||||
_ = self.evalScript(&script);
|
||||
},
|
||||
.undef => {
|
||||
// a custom element
|
||||
const js_obj = self.main_context.getJsObject(element) orelse return;
|
||||
|
||||
// @memory
|
||||
// getFunction, and more generally Env.Function always create
|
||||
// a Persisted Object. But, in cases like this, we don't need
|
||||
// it to persist.
|
||||
const func = (try js_obj.getFunction("connectedCallback")) orelse return;
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
func.tryCallWithThis(void, js_obj, .{}, &result) catch {
|
||||
log.warn(.user_script, "connected callback", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
});
|
||||
};
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const DelayedNavigation = struct {
|
||||
url: []const u8,
|
||||
url: URL,
|
||||
session: *Session,
|
||||
opts: NavigateOpts,
|
||||
initial: bool = true,
|
||||
navigate_node: Loop.CallbackNode = .{ .func = delayNavigate },
|
||||
|
||||
// Navigation is blocking, which is problem because it can seize up
|
||||
// the loop and deadlock. We can only safely try to navigate to a
|
||||
// new page when we're sure there's at least 1 free slot in the
|
||||
// http client. We handle this in two phases:
|
||||
//
|
||||
// In the first phase, when self.initial == true, we'll shutdown the page
|
||||
// and create a new one. The shutdown is important, because it resets the
|
||||
// loop ctx_id and removes the JsContext. Removing the context calls our XHR
|
||||
// destructors which aborts requests. This is necessary to make sure our
|
||||
// [blocking] navigate won't block.
|
||||
//
|
||||
// In the 2nd phase, we wait until there's a free http slot so that our
|
||||
// navigate definetly won't block (which could deadlock the system if there
|
||||
// are still pending async requests, which we've seen happen, even after
|
||||
// an abort).
|
||||
fn delayNavigate(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
||||
_ = repeat_delay;
|
||||
const self: *DelayedNavigation = @fieldParentPtr("navigate_node", node);
|
||||
self.session.pageNavigate(self.url, self.opts) catch |err| {
|
||||
|
||||
const session = self.session;
|
||||
const initial = self.initial;
|
||||
|
||||
if (initial) {
|
||||
// Prior to schedule this task, we terminated excution to stop
|
||||
// the running script. If we don't resume it before doing a shutdown
|
||||
// we'll get an error.
|
||||
session.executor.resumeExecution();
|
||||
|
||||
session.removePage();
|
||||
_ = session.createPage() catch |err| {
|
||||
log.err(.browser, "delayed navigation page error", .{
|
||||
.err = err,
|
||||
.url = self.url,
|
||||
});
|
||||
return;
|
||||
};
|
||||
self.initial = false;
|
||||
}
|
||||
|
||||
if (session.browser.http_client.freeSlotCount() == 0) {
|
||||
log.debug(.browser, "delayed navigate waiting", .{});
|
||||
const delay = 0 * std.time.ns_per_ms;
|
||||
|
||||
// If this isn't the initial check, we can safely re-use the timer
|
||||
// to check again.
|
||||
if (initial == false) {
|
||||
repeat_delay.* = delay;
|
||||
return;
|
||||
}
|
||||
|
||||
// However, if this _is_ the initial check, we called
|
||||
// session.removePage above, and that reset the loop ctx_id.
|
||||
// We can't re-use this timer, because it has the previous ctx_id.
|
||||
// We can create a new timeout though, and that'll get the new ctx_id.
|
||||
//
|
||||
// Page has to be not-null here because we called createPage above.
|
||||
_ = session.page.?.loop.timeout(delay, &self.navigate_node) catch |err| {
|
||||
log.err(.browser, "delayed navigation loop err", .{ .err = err });
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const page = session.currentPage() orelse return;
|
||||
defer if (!page.delayed_navigation) {
|
||||
// If, while loading the page, we intend to navigate to another
|
||||
// page, then we need to keep the transfer_arena around, as this
|
||||
// sub-navigation is probably using it.
|
||||
_ = session.browser.transfer_arena.reset(.{ .retain_with_limit = 64 * 1024 });
|
||||
};
|
||||
|
||||
return page.navigate(self.url, self.opts) catch |err| {
|
||||
log.err(.browser, "delayed navigation error", .{ .err = err, .url = self.url });
|
||||
};
|
||||
}
|
||||
@@ -802,14 +1035,14 @@ const Script = struct {
|
||||
|
||||
fn eval(self: *const Script, page: *Page, body: []const u8) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(page.scope);
|
||||
try_catch.init(page.main_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const src = self.src orelse "inline";
|
||||
_ = switch (self.kind) {
|
||||
.javascript => page.scope.exec(body, src),
|
||||
.javascript => page.main_context.exec(body, src),
|
||||
.module => blk: {
|
||||
switch (try page.scope.module(body, src)) {
|
||||
switch (try page.main_context.module(body, src)) {
|
||||
.value => |v| break :blk v,
|
||||
.exception => |e| {
|
||||
log.warn(.user_script, "eval module", .{
|
||||
@@ -821,9 +1054,17 @@ const Script = struct {
|
||||
}
|
||||
},
|
||||
} catch {
|
||||
if (try try_catch.err(page.arena)) |msg| {
|
||||
log.warn(.user_script, "eval script", .{ .src = src, .err = msg });
|
||||
if (page.delayed_navigation) {
|
||||
return error.Terminated;
|
||||
}
|
||||
|
||||
if (try try_catch.err(page.arena)) |msg| {
|
||||
log.warn(.user_script, "eval script", .{
|
||||
.src = src,
|
||||
.err = msg,
|
||||
});
|
||||
}
|
||||
|
||||
try self.executeCallback("onerror", page);
|
||||
return error.JsErr;
|
||||
};
|
||||
@@ -835,9 +1076,9 @@ const Script = struct {
|
||||
switch (callback) {
|
||||
.string => |str| {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(page.scope);
|
||||
try_catch.init(page.main_context);
|
||||
defer try_catch.deinit();
|
||||
_ = page.scope.exec(str, typ) catch {
|
||||
_ = page.main_context.exec(str, typ) catch {
|
||||
if (try try_catch.err(page.arena)) |msg| {
|
||||
log.warn(.user_script, "script callback", .{
|
||||
.src = self.src,
|
||||
@@ -887,19 +1128,22 @@ fn timestamp() u32 {
|
||||
return @intCast(ts.sec);
|
||||
}
|
||||
|
||||
// A callback from libdom whenever a script tag is added to the DOM.
|
||||
// A callback from libdom whenever an html element is added to the DOM.
|
||||
// Note that "added" could mean that it was removed from one parent and re-added
|
||||
// to another, which is how MOST APIs implement "move" (corrently so).
|
||||
//
|
||||
// The only API which seems to actual "move" is Element.moveBefore, which we
|
||||
// don't currently implement, but should support in general, and should handle
|
||||
// specifically here.
|
||||
// element is guaranteed to be a script element.
|
||||
// The script tag might not have a src. It might be any attribute, like
|
||||
// The script tag might not have a src. It might have any attribute, like
|
||||
// `nomodule`, `defer` and `async`. `Script.init` will return null on `nomodule`
|
||||
// so that's handled. And because we're only executing the inline <script> tags
|
||||
// after the document is loaded, it's ok to execute any async and defer scripts
|
||||
// immediately.
|
||||
pub export fn scriptAddedCallback(ctx: ?*anyopaque, element: ?*parser.Element) callconv(.C) void {
|
||||
pub export fn elementAddedCallback(ctx: ?*anyopaque, element: ?*parser.Element) callconv(.C) void {
|
||||
const self: *Page = @alignCast(@ptrCast(ctx.?));
|
||||
var script = Script.init(element.?, self) catch |err| {
|
||||
log.warn(.browser, "script added init error", .{ .err = err });
|
||||
return;
|
||||
} orelse return;
|
||||
|
||||
self.evalScript(&script);
|
||||
self.elementAdded(element.?) catch |err| {
|
||||
log.warn(.browser, "element added callback", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ test "Browser.fetch" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
|
||||
try @import("polyfill.zig").load(testing.allocator, runner.page.scope);
|
||||
try @import("polyfill.zig").load(testing.allocator, runner.page.main_context);
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
|
||||
@@ -30,13 +30,13 @@ const modules = [_]struct {
|
||||
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
|
||||
};
|
||||
|
||||
pub fn load(allocator: Allocator, scope: *Env.Scope) !void {
|
||||
pub fn load(allocator: Allocator, js_context: *Env.JsContext) !void {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(scope);
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
for (modules) |m| {
|
||||
_ = scope.exec(m.source, m.name) catch |err| {
|
||||
_ = 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 });
|
||||
|
||||
@@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const Env = @import("env.zig").Env;
|
||||
const Page = @import("page.zig").Page;
|
||||
const URL = @import("../url.zig").URL;
|
||||
const Browser = @import("browser.zig").Browser;
|
||||
const NavigateOpts = @import("page.zig").NavigateOpts;
|
||||
|
||||
@@ -72,7 +73,7 @@ pub const Session = struct {
|
||||
|
||||
pub fn deinit(self: *Session) void {
|
||||
if (self.page != null) {
|
||||
self.removePage() catch {};
|
||||
self.removePage();
|
||||
}
|
||||
self.cookie_jar.deinit();
|
||||
self.storage_shed.deinit();
|
||||
@@ -104,7 +105,7 @@ pub const Session = struct {
|
||||
return page;
|
||||
}
|
||||
|
||||
pub fn removePage(self: *Session) !void {
|
||||
pub fn removePage(self: *Session) void {
|
||||
// Inform CDP the page is going to be removed, allowing other worlds to remove themselves before the main one
|
||||
self.browser.notification.dispatch(.page_remove, .{});
|
||||
|
||||
@@ -115,11 +116,11 @@ pub const Session = struct {
|
||||
// phase. It's important that we clean these up, as they're holding onto
|
||||
// limited resources (like our fixed-sized http state pool).
|
||||
//
|
||||
// First thing we do, is endScope() which will execute the destructor
|
||||
// First thing we do, is removeJsContext() which will execute the destructor
|
||||
// of any type that registered a destructor (e.g. XMLHttpRequest).
|
||||
// This will shutdown any pending sockets, which begins our cleaning
|
||||
// processed
|
||||
self.executor.endScope();
|
||||
self.executor.removeJsContext();
|
||||
|
||||
// Second thing we do is reset the loop. This increments the loop ctx_id
|
||||
// so that any "stale" timeouts we process will get ignored. We need to
|
||||
@@ -127,12 +128,6 @@ pub const Session = struct {
|
||||
// window.setTimeout and running microtasks should be ignored
|
||||
self.browser.app.loop.reset();
|
||||
|
||||
// Finally, we run the loop. Because of the reset just above, this will
|
||||
// ignore any timeouts. And, because of the endScope about this, it
|
||||
// should ensure that the http requests detect the shutdown socket and
|
||||
// release their resources.
|
||||
try self.browser.app.loop.run();
|
||||
|
||||
self.page = null;
|
||||
|
||||
// clear netsurf memory arena.
|
||||
@@ -144,28 +139,4 @@ pub const Session = struct {
|
||||
pub fn currentPage(self: *Session) ?*Page {
|
||||
return &(self.page orelse return null);
|
||||
}
|
||||
|
||||
pub fn pageNavigate(self: *Session, url_string: []const u8, opts: NavigateOpts) !void {
|
||||
// currently, this is only called from the page, so let's hope
|
||||
// it isn't null!
|
||||
std.debug.assert(self.page != null);
|
||||
|
||||
defer if (self.page) |*p| {
|
||||
if (!p.delayed_navigation) {
|
||||
// If, while loading the page, we intend to navigate to another
|
||||
// page, then we need to keep the transfer_arena around, as this
|
||||
// sub-navigation is probably using it.
|
||||
_ = self.browser.transfer_arena.reset(.{ .retain_with_limit = 64 * 1024 });
|
||||
}
|
||||
};
|
||||
|
||||
// it's safe to use the transfer arena here, because the page will
|
||||
// eventually clone the URL using its own page_arena (after it gets
|
||||
// the final URL, possibly following redirects)
|
||||
const url = try self.page.?.url.resolve(self.transfer_arena, url_string);
|
||||
|
||||
try self.removePage();
|
||||
var page = try self.createPage();
|
||||
return page.navigate(url, opts);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ pub const Interfaces = .{
|
||||
// allocatorate data, I should be able to retrieve the scheme + the following `:`
|
||||
// from rawuri.
|
||||
//
|
||||
// 2. The other way would bu to copy the `std.Uri` code to ahve a dedicated
|
||||
// 2. The other way would be to copy the `std.Uri` code to have a dedicated
|
||||
// parser including the characters we want for the web API.
|
||||
pub const URL = struct {
|
||||
uri: std.Uri,
|
||||
|
||||
115
src/browser/webcomponents/custom_element_registry.zig
Normal file
115
src/browser/webcomponents/custom_element_registry.zig
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright (C) 2023-2024 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 v8 = @import("v8");
|
||||
|
||||
const Env = @import("../env.zig").Env;
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
pub const CustomElementRegistry = struct {
|
||||
// tag_name -> Function
|
||||
lookup: std.StringHashMapUnmanaged(Env.Function) = .empty,
|
||||
|
||||
pub fn _define(self: *CustomElementRegistry, tag_name: []const u8, fun: Env.Function, page: *Page) !void {
|
||||
log.info(.browser, "define custom element", .{ .name = tag_name });
|
||||
|
||||
const arena = page.arena;
|
||||
const gop = try self.lookup.getOrPut(arena, tag_name);
|
||||
if (!gop.found_existing) {
|
||||
errdefer _ = self.lookup.remove(tag_name);
|
||||
const owned_tag_name = try arena.dupe(u8, tag_name);
|
||||
gop.key_ptr.* = owned_tag_name;
|
||||
}
|
||||
gop.value_ptr.* = fun;
|
||||
fun.setName(tag_name);
|
||||
}
|
||||
|
||||
pub fn _get(self: *CustomElementRegistry, name: []const u8) ?Env.Function {
|
||||
return self.lookup.get(name);
|
||||
}
|
||||
|
||||
pub fn newInstance(self: *const CustomElementRegistry, tag_name: []const u8) !?Env.JsObject {
|
||||
const func = self.lookup.get(tag_name) orelse return null;
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
const js_obj = func.newInstance(&result) catch |err| {
|
||||
log.fatal(.user_script, "newInstance error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.tag_name = tag_name,
|
||||
.source = "createElement",
|
||||
});
|
||||
return err;
|
||||
};
|
||||
|
||||
// This is associated with an HTML element, which, at the very least
|
||||
// is going to be libdom node. It will outlive this call, and thus needs
|
||||
// to be persisted.
|
||||
return try js_obj.persist();
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
test "Browser.CustomElementRegistry" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
defer runner.deinit();
|
||||
try runner.testCases(&.{
|
||||
// Basic registry access
|
||||
.{ "typeof customElements", "object" },
|
||||
.{ "customElements instanceof CustomElementRegistry", "true" },
|
||||
|
||||
// Define a simple custom element
|
||||
.{
|
||||
\\ class MyElement extends HTMLElement {
|
||||
\\ constructor() {
|
||||
\\ super();
|
||||
\\ this.textContent = 'created';
|
||||
\\ }
|
||||
\\
|
||||
\\ connectedCallback() {
|
||||
\\ this.textContent = 'connected';
|
||||
\\ }
|
||||
\\ }
|
||||
,
|
||||
null,
|
||||
},
|
||||
.{ "customElements.define('my-element', MyElement)", "undefined" },
|
||||
|
||||
// Check if element is defined
|
||||
.{ "customElements.get('my-element') === MyElement", "true" },
|
||||
// .{ "customElements.get('non-existent')", "null" },
|
||||
|
||||
// Create element via document.createElement
|
||||
.{ "let el = document.createElement('my-element')", "undefined" },
|
||||
.{ "el instanceof MyElement", "true" },
|
||||
.{ "el instanceof HTMLElement", "true" },
|
||||
.{ "el.tagName", "MY-ELEMENT" },
|
||||
.{ "el.textContent", "created" },
|
||||
.{ "document.getElementsByTagName('body')[0].append(el)", null },
|
||||
.{ "el.textContent", "connected" },
|
||||
|
||||
// Create element via HTML parsing
|
||||
// .{ "document.body.innerHTML = '<my-element></my-element>'", "undefined" },
|
||||
// .{ "let parsed = document.querySelector('my-element')", "undefined" },
|
||||
// .{ "parsed instanceof MyElement", "true" },
|
||||
// .{ "parsed.textContent", "Hello World" },
|
||||
}, .{});
|
||||
}
|
||||
23
src/browser/webcomponents/webcomponents.zig
Normal file
23
src/browser/webcomponents/webcomponents.zig
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (C) 2023-2024 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 CustomElementRegistry = @import("custom_element_registry.zig").CustomElementRegistry;
|
||||
|
||||
pub const Interfaces = .{
|
||||
CustomElementRegistry,
|
||||
};
|
||||
@@ -115,17 +115,24 @@ const EntryIterable = iterator.Iterable(kv.EntryIterator, "FormDataEntryIterator
|
||||
// TODO: handle disabled fieldsets
|
||||
fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !kv.List {
|
||||
const arena = page.arena;
|
||||
const collection = try parser.formGetCollection(form);
|
||||
const len = try parser.htmlCollectionGetLength(collection);
|
||||
|
||||
// Don't use libdom's formGetCollection (aka dom_html_form_element_get_elements)
|
||||
// It doesn't work with dynamically added elements, because their form
|
||||
// property doesn't get set. We should fix that.
|
||||
// However, even once fixed, there are other form-collection features we
|
||||
// probably want to implement (like disabled fieldsets), so we might want
|
||||
// to stick with our own walker even if fix libdom to properly support
|
||||
// dynamically added elements.
|
||||
const node_list = try @import("../dom/css.zig").querySelectorAll(arena, @alignCast(@ptrCast(form)), "input,select,button,textarea");
|
||||
const nodes = node_list.nodes.items;
|
||||
|
||||
var entries: kv.List = .{};
|
||||
try entries.ensureTotalCapacity(arena, len);
|
||||
try entries.ensureTotalCapacity(arena, nodes.len);
|
||||
|
||||
var submitter_included = false;
|
||||
const submitter_name_ = try getSubmitterName(submitter_);
|
||||
|
||||
for (0..len) |i| {
|
||||
const node = try parser.htmlCollectionItem(collection, @intCast(i));
|
||||
for (nodes) |node| {
|
||||
const element = parser.nodeToElement(node);
|
||||
|
||||
// must have a name
|
||||
@@ -137,7 +144,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(element)));
|
||||
switch (tag) {
|
||||
.input => {
|
||||
const tpe = try parser.elementGetAttribute(element, "type") orelse "";
|
||||
const tpe = try parser.inputGetType(@ptrCast(element));
|
||||
if (std.ascii.eqlIgnoreCase(tpe, "image")) {
|
||||
if (submitter_name_) |submitter_name| {
|
||||
if (std.mem.eql(u8, submitter_name, name)) {
|
||||
@@ -162,7 +169,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
|
||||
}
|
||||
submitter_included = true;
|
||||
}
|
||||
const value = (try parser.elementGetAttribute(element, "value")) orelse "";
|
||||
const value = try parser.inputGetValue(@ptrCast(element));
|
||||
try entries.appendOwned(arena, name, value);
|
||||
},
|
||||
.select => {
|
||||
@@ -181,19 +188,16 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
|
||||
submitter_included = true;
|
||||
}
|
||||
},
|
||||
else => {
|
||||
log.warn(.web_api, "unsupported form element", .{ .tag = @tagName(tag) });
|
||||
continue;
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
if (submitter_included == false) {
|
||||
if (submitter_) |submitter| {
|
||||
if (submitter_name_) |submitter_name| {
|
||||
// this can happen if the submitter is outside the form, but associated
|
||||
// with the form via a form=ID attribute
|
||||
const value = (try parser.elementGetAttribute(@ptrCast(submitter), "value")) orelse "";
|
||||
try entries.appendOwned(arena, submitter_name_.?, value);
|
||||
const value = (try parser.elementGetAttribute(@ptrCast(submitter_.?), "value")) orelse "";
|
||||
try entries.appendOwned(arena, submitter_name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +220,7 @@ fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u
|
||||
if (is_multiple == false) {
|
||||
const option = try parser.optionCollectionItem(options, @intCast(selected_index));
|
||||
|
||||
if (try parser.elementGetAttribute(@ptrCast(option), "disabled") != null) {
|
||||
if (try parser.elementGetAttribute(@alignCast(@ptrCast(option)), "disabled") != null) {
|
||||
return;
|
||||
}
|
||||
const value = try parser.optionGetValue(option);
|
||||
@@ -228,7 +232,7 @@ fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u
|
||||
// we can go directly to the first one
|
||||
for (@intCast(selected_index)..len) |i| {
|
||||
const option = try parser.optionCollectionItem(options, @intCast(i));
|
||||
if (try parser.elementGetAttribute(@ptrCast(option), "disabled") != null) {
|
||||
if (try parser.elementGetAttribute(@alignCast(@ptrCast(option)), "disabled") != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -249,7 +253,7 @@ fn getSubmitterName(submitter_: ?*parser.ElementHTML) !?[]const u8 {
|
||||
switch (tag) {
|
||||
.button => return name,
|
||||
.input => {
|
||||
const tpe = (try parser.elementGetAttribute(element, "type")) orelse "";
|
||||
const tpe = try parser.inputGetType(@ptrCast(element));
|
||||
// only an image type can be a sumbitter
|
||||
if (std.ascii.eqlIgnoreCase(tpe, "image") or std.ascii.eqlIgnoreCase(tpe, "submit")) {
|
||||
return name;
|
||||
@@ -297,6 +301,7 @@ test "Browser.FormData" {
|
||||
\\ <input type=submit name=s2 value=s2-v>
|
||||
\\ <input type=image name=i1 value=i1-v>
|
||||
\\ </form>
|
||||
\\ <input type=text name=abc value=123 form=form1>
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
@@ -356,6 +361,8 @@ test "Browser.FormData" {
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let form1 = document.getElementById('form1')", null },
|
||||
.{ "let input = document.createElement('input');", null },
|
||||
.{ "input.name = 'dyn'; input.value= 'dyn-v'; form1.appendChild(input);", null },
|
||||
.{ "let submit1 = document.getElementById('s1')", null },
|
||||
.{ "let f2 = new FormData(form1, submit1)", null },
|
||||
.{ "acc = '';", null },
|
||||
@@ -378,6 +385,7 @@ test "Browser.FormData" {
|
||||
\\mlt-2=water
|
||||
\\mlt-2=tea
|
||||
\\s1=s1-v
|
||||
\\dyn=dyn-v
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -338,9 +338,20 @@ pub const XMLHttpRequest = struct {
|
||||
// dispatch request event.
|
||||
// errors are logged only.
|
||||
fn dispatchEvt(self: *XMLHttpRequest, typ: []const u8) void {
|
||||
log.debug(.script_event, "dispatch event", .{ .type = typ, .source = "xhr" });
|
||||
log.debug(.script_event, "dispatch event", .{
|
||||
.type = typ,
|
||||
.source = "xhr",
|
||||
.method = self.method,
|
||||
.url = self.url,
|
||||
});
|
||||
self._dispatchEvt(typ) catch |err| {
|
||||
log.err(.app, "dispatch event error", .{ .err = err, .type = typ, .source = "xhr" });
|
||||
log.err(.app, "dispatch event error", .{
|
||||
.err = err,
|
||||
.type = typ,
|
||||
.source = "xhr",
|
||||
.method = self.method,
|
||||
.url = self.url,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -358,9 +369,20 @@ pub const XMLHttpRequest = struct {
|
||||
typ: []const u8,
|
||||
opts: ProgressEvent.EventInit,
|
||||
) void {
|
||||
log.debug(.script_event, "dispatch progress event", .{ .type = typ, .source = "xhr" });
|
||||
log.debug(.script_event, "dispatch progress event", .{
|
||||
.type = typ,
|
||||
.source = "xhr",
|
||||
.method = self.method,
|
||||
.url = self.url,
|
||||
});
|
||||
self._dispatchProgressEvent(typ, opts) catch |err| {
|
||||
log.err(.app, "dispatch progress event error", .{ .err = err, .type = typ, .source = "xhr" });
|
||||
log.err(.app, "dispatch progress event error", .{
|
||||
.err = err,
|
||||
.type = typ,
|
||||
.source = "xhr",
|
||||
.method = self.method,
|
||||
.url = self.url,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ const json = std.json;
|
||||
const log = @import("../log.zig");
|
||||
const App = @import("../app.zig").App;
|
||||
const Env = @import("../browser/env.zig").Env;
|
||||
const asUint = @import("../str/parser.zig").asUint;
|
||||
const Browser = @import("../browser/browser.zig").Browser;
|
||||
const Session = @import("../browser/session.zig").Session;
|
||||
const Page = @import("../browser/page.zig").Page;
|
||||
@@ -182,41 +181,41 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
|
||||
switch (domain.len) {
|
||||
3 => switch (@as(u24, @bitCast(domain[0..3].*))) {
|
||||
asUint("DOM") => return @import("domains/dom.zig").processMessage(command),
|
||||
asUint("Log") => return @import("domains/log.zig").processMessage(command),
|
||||
asUint("CSS") => return @import("domains/css.zig").processMessage(command),
|
||||
asUint(u24, "DOM") => return @import("domains/dom.zig").processMessage(command),
|
||||
asUint(u24, "Log") => return @import("domains/log.zig").processMessage(command),
|
||||
asUint(u24, "CSS") => return @import("domains/css.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
4 => switch (@as(u32, @bitCast(domain[0..4].*))) {
|
||||
asUint("Page") => return @import("domains/page.zig").processMessage(command),
|
||||
asUint(u32, "Page") => return @import("domains/page.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
5 => switch (@as(u40, @bitCast(domain[0..5].*))) {
|
||||
asUint("Fetch") => return @import("domains/fetch.zig").processMessage(command),
|
||||
asUint("Input") => return @import("domains/input.zig").processMessage(command),
|
||||
asUint(u40, "Fetch") => return @import("domains/fetch.zig").processMessage(command),
|
||||
asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(domain[0..6].*))) {
|
||||
asUint("Target") => return @import("domains/target.zig").processMessage(command),
|
||||
asUint(u48, "Target") => return @import("domains/target.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
7 => switch (@as(u56, @bitCast(domain[0..7].*))) {
|
||||
asUint("Browser") => return @import("domains/browser.zig").processMessage(command),
|
||||
asUint("Runtime") => return @import("domains/runtime.zig").processMessage(command),
|
||||
asUint("Network") => return @import("domains/network.zig").processMessage(command),
|
||||
asUint(u56, "Browser") => return @import("domains/browser.zig").processMessage(command),
|
||||
asUint(u56, "Runtime") => return @import("domains/runtime.zig").processMessage(command),
|
||||
asUint(u56, "Network") => return @import("domains/network.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
8 => switch (@as(u64, @bitCast(domain[0..8].*))) {
|
||||
asUint("Security") => return @import("domains/security.zig").processMessage(command),
|
||||
asUint(u64, "Security") => return @import("domains/security.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
9 => switch (@as(u72, @bitCast(domain[0..9].*))) {
|
||||
asUint("Emulation") => return @import("domains/emulation.zig").processMessage(command),
|
||||
asUint("Inspector") => return @import("domains/inspector.zig").processMessage(command),
|
||||
asUint(u72, "Emulation") => return @import("domains/emulation.zig").processMessage(command),
|
||||
asUint(u72, "Inspector") => return @import("domains/inspector.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
11 => switch (@as(u88, @bitCast(domain[0..11].*))) {
|
||||
asUint("Performance") => return @import("domains/performance.zig").processMessage(command),
|
||||
asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
@@ -412,11 +411,13 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
}
|
||||
|
||||
pub fn networkEnable(self: *Self) !void {
|
||||
try self.cdp.browser.notification.register(.http_request_fail, self, onHttpRequestFail);
|
||||
try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart);
|
||||
try self.cdp.browser.notification.register(.http_request_complete, self, onHttpRequestComplete);
|
||||
}
|
||||
|
||||
pub fn networkDisable(self: *Self) void {
|
||||
self.cdp.browser.notification.unregister(.http_request_fail, self);
|
||||
self.cdp.browser.notification.unregister(.http_request_start, self);
|
||||
self.cdp.browser.notification.unregister(.http_request_complete, self);
|
||||
}
|
||||
@@ -448,6 +449,12 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
return @import("domains/network.zig").httpRequestStart(self.notification_arena, self, data);
|
||||
}
|
||||
|
||||
pub fn onHttpRequestFail(ctx: *anyopaque, data: *const Notification.RequestFail) !void {
|
||||
const self: *Self = @alignCast(@ptrCast(ctx));
|
||||
defer self.resetNotificationArena();
|
||||
return @import("domains/network.zig").httpRequestFail(self.notification_arena, self, data);
|
||||
}
|
||||
|
||||
pub fn onHttpRequestComplete(ctx: *anyopaque, data: *const Notification.RequestComplete) !void {
|
||||
const self: *Self = @alignCast(@ptrCast(ctx));
|
||||
defer self.resetNotificationArena();
|
||||
@@ -547,8 +554,8 @@ const IsolatedWorld = struct {
|
||||
self.executor.deinit();
|
||||
}
|
||||
pub fn removeContext(self: *IsolatedWorld) !void {
|
||||
if (self.executor.scope == null) return error.NoIsolatedContextToRemove;
|
||||
self.executor.endScope();
|
||||
if (self.executor.js_context == null) return error.NoIsolatedContextToRemove;
|
||||
self.executor.removeJsContext();
|
||||
}
|
||||
|
||||
// The isolate world must share at least some of the state with the related page, specifically the DocumentHTML
|
||||
@@ -557,8 +564,8 @@ const IsolatedWorld = struct {
|
||||
// This also means this pointer becomes invalid after removePage untill a new page is created.
|
||||
// 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.scope != null) return error.Only1IsolatedContextSupported;
|
||||
_ = try self.executor.startScope(&page.window, page, {}, false);
|
||||
if (self.executor.js_context != null) return error.Only1IsolatedContextSupported;
|
||||
_ = try self.executor.createJsContext(&page.window, page, {}, false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -688,6 +695,10 @@ const InputParams = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn asUint(comptime T: type, comptime string: []const u8) T {
|
||||
return @bitCast(string[0..string.len].*);
|
||||
}
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "cdp: invalid json" {
|
||||
var ctx = testing.context();
|
||||
|
||||
@@ -259,13 +259,13 @@ fn resolveNode(cmd: anytype) !void {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
|
||||
var scope = page.scope;
|
||||
var js_context = page.main_context;
|
||||
if (params.executionContextId) |context_id| {
|
||||
if (scope.context.debugContextId() != context_id) {
|
||||
if (js_context.v8_context.debugContextId() != context_id) {
|
||||
var isolated_world = bc.isolated_world orelse return error.ContextNotFound;
|
||||
scope = &(isolated_world.executor.scope orelse return error.ContextNotFound);
|
||||
js_context = &(isolated_world.executor.js_context orelse return error.ContextNotFound);
|
||||
|
||||
if (scope.context.debugContextId() != context_id) return error.ContextNotFound;
|
||||
if (js_context.v8_context.debugContextId() != context_id) return error.ContextNotFound;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ fn resolveNode(cmd: anytype) !void {
|
||||
// node._node is a *parser.Node we need this to be able to find its most derived type e.g. Node -> Element -> HTMLElement
|
||||
// So we use the Node.Union when retrieve the value from the environment
|
||||
const remote_object = try bc.inspector.getRemoteObject(
|
||||
scope,
|
||||
js_context,
|
||||
params.objectGroup orelse "",
|
||||
try dom_node.Node.toInterface(node._node),
|
||||
);
|
||||
@@ -368,7 +368,7 @@ fn getNode(arena: Allocator, browser_context: anytype, node_id: ?Node.Id, backen
|
||||
if (object_id) |object_id_| {
|
||||
// Retrieve the object from which ever context it is in.
|
||||
const parser_node = try browser_context.inspector.getNodePtr(arena, object_id_);
|
||||
return try browser_context.node_registry.register(@ptrCast(parser_node));
|
||||
return try browser_context.node_registry.register(@alignCast(@ptrCast(parser_node)));
|
||||
}
|
||||
return error.MissingParams;
|
||||
}
|
||||
|
||||
@@ -21,14 +21,60 @@ const Page = @import("../../browser/page.zig").Page;
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
dispatchKeyEvent,
|
||||
dispatchMouseEvent,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.dispatchKeyEvent => return dispatchKeyEvent(cmd),
|
||||
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
|
||||
}
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
|
||||
fn dispatchKeyEvent(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
type: Type,
|
||||
key: []const u8 = "",
|
||||
code: []const u8 = "",
|
||||
modifiers: u4 = 0,
|
||||
// Many optional parameters are not implemented yet, see documentation url.
|
||||
|
||||
const Type = enum {
|
||||
keyDown,
|
||||
keyUp,
|
||||
rawKeyDown,
|
||||
char,
|
||||
};
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
try cmd.sendResult(null, .{});
|
||||
|
||||
// quickly ignore types we know we don't handle
|
||||
switch (params.type) {
|
||||
.keyUp, .rawKeyDown, .char => return,
|
||||
.keyDown => {},
|
||||
}
|
||||
|
||||
const bc = cmd.browser_context orelse return;
|
||||
const page = bc.session.currentPage() orelse return;
|
||||
|
||||
const keyboard_event = Page.KeyboardEvent{
|
||||
.key = params.key,
|
||||
.code = params.code,
|
||||
.type = switch (params.type) {
|
||||
.keyDown => .keydown,
|
||||
else => unreachable,
|
||||
},
|
||||
.alt = params.modifiers & 1 == 1,
|
||||
.ctrl = params.modifiers & 2 == 2,
|
||||
.meta = params.modifiers & 4 == 4,
|
||||
.shift = params.modifiers & 8 == 8,
|
||||
};
|
||||
try page.keyboardEvent(keyboard_event);
|
||||
// result already sent
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
|
||||
fn dispatchMouseEvent(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
|
||||
@@ -84,6 +84,26 @@ fn putAssumeCapacity(headers: *std.ArrayListUnmanaged(std.http.Header), extra: s
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn httpRequestFail(arena: Allocator, bc: anytype, request: *const Notification.RequestFail) !void {
|
||||
// It's possible that the request failed because we aborted when the client
|
||||
// sent Target.closeTarget. In that case, bc.session_id will be cleared
|
||||
// already, and we can skip sending these messages to the client.
|
||||
const session_id = bc.session_id orelse return;
|
||||
|
||||
// Isn't possible to do a network request within a Browser (which our
|
||||
// notification is tied to), without a page.
|
||||
std.debug.assert(bc.session.page != null);
|
||||
|
||||
// We're missing a bunch of fields, but, for now, this seems like enough
|
||||
try bc.cdp.sendEvent("Network.loadingFailed", .{
|
||||
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{request.id}),
|
||||
// Seems to be what chrome answers with. I assume it depends on the type of error?
|
||||
.type = "Ping",
|
||||
.errorText = request.err,
|
||||
.canceled = false,
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
pub fn httpRequestStart(arena: Allocator, bc: anytype, request: *const Notification.RequestStart) !void {
|
||||
// Isn't possible to do a network request within a Browser (which our
|
||||
// notification is tied to), without a page.
|
||||
|
||||
@@ -117,14 +117,14 @@ fn createIsolatedWorld(cmd: anytype) !void {
|
||||
const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
try pageCreated(bc, page);
|
||||
const scope = &world.executor.scope.?;
|
||||
const js_context = &world.executor.js_context.?;
|
||||
|
||||
// Create the auxdata json for the contextCreated event
|
||||
// Calling contextCreated will assign a Id to the context and send the contextCreated event
|
||||
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{params.frameId});
|
||||
bc.inspector.contextCreated(scope, world.name, "", aux_data, false);
|
||||
bc.inspector.contextCreated(js_context, world.name, "", aux_data, false);
|
||||
|
||||
return cmd.sendResult(.{ .executionContextId = scope.context.debugContextId() }, .{});
|
||||
return cmd.sendResult(.{ .executionContextId = js_context.v8_context.debugContextId() }, .{});
|
||||
}
|
||||
|
||||
fn navigate(cmd: anytype) !void {
|
||||
@@ -163,6 +163,11 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
||||
std.debug.assert(bc.session.page != null);
|
||||
|
||||
var cdp = bc.cdp;
|
||||
|
||||
if (event.opts.reason != .address_bar) {
|
||||
bc.loader_id = bc.cdp.loader_id_gen.next();
|
||||
}
|
||||
|
||||
const loader_id = bc.loader_id;
|
||||
const target_id = bc.target_id orelse unreachable;
|
||||
const session_id = bc.session_id orelse unreachable;
|
||||
@@ -248,7 +253,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
bc.inspector.contextCreated(
|
||||
page.scope,
|
||||
page.main_context,
|
||||
"",
|
||||
try page.origin(arena),
|
||||
aux_data,
|
||||
@@ -259,7 +264,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
|
||||
const aux_json = try std.fmt.allocPrint(arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
// Calling contextCreated will assign a new Id to the context and send the contextCreated event
|
||||
bc.inspector.contextCreated(
|
||||
&isolated_world.executor.scope.?,
|
||||
&isolated_world.executor.js_context.?,
|
||||
isolated_world.name,
|
||||
"://",
|
||||
aux_json,
|
||||
@@ -281,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.load(bc.arena, &isolated_world.executor.scope.?);
|
||||
try polyfill.load(bc.arena, &isolated_world.executor.js_context.?);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ fn createTarget(cmd: anytype) !void {
|
||||
{
|
||||
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
|
||||
bc.inspector.contextCreated(
|
||||
page.scope,
|
||||
page.main_context,
|
||||
"",
|
||||
try page.origin(cmd.arena),
|
||||
aux_data,
|
||||
@@ -220,7 +220,7 @@ fn closeTarget(cmd: anytype) !void {
|
||||
bc.session_id = null;
|
||||
}
|
||||
|
||||
try bc.session.removePage();
|
||||
bc.session.removePage();
|
||||
if (bc.isolated_world) |*world| {
|
||||
world.deinit();
|
||||
bc.isolated_world = null;
|
||||
|
||||
@@ -113,10 +113,18 @@ pub const Client = struct {
|
||||
loop: *Loop,
|
||||
opts: RequestOpts,
|
||||
) !void {
|
||||
if (self.state_pool.acquireOrNull()) |state| {
|
||||
// if we have state ready, we can skip the loop and immediately
|
||||
// kick this request off.
|
||||
return self.asyncRequestReady(method, uri, ctx, callback, state, opts);
|
||||
|
||||
// See the page's DelayedNavitation for why we're doing this. TL;DR -
|
||||
// we need to keep 1 slot available for the blocking page navigation flow
|
||||
// (Almost worth keeping a dedicate State just for that flow, but keep
|
||||
// thinking we need a more permanent solution (i.e. making everything
|
||||
// non-blocking).
|
||||
if (self.freeSlotCount() > 1) {
|
||||
if (self.state_pool.acquireOrNull()) |state| {
|
||||
// if we have state ready, we can skip the loop and immediately
|
||||
// kick this request off.
|
||||
return self.asyncRequestReady(method, uri, ctx, callback, state, opts);
|
||||
}
|
||||
}
|
||||
|
||||
// This cannot be a client-owned MemoryPool. The page can end before
|
||||
@@ -174,6 +182,10 @@ pub const Client = struct {
|
||||
.client = self,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn freeSlotCount(self: *Client) usize {
|
||||
return self.state_pool.freeSlotCount();
|
||||
}
|
||||
};
|
||||
|
||||
const RequestOpts = struct {
|
||||
@@ -354,6 +366,7 @@ pub const Request = struct {
|
||||
// Because of things like redirects and error handling, it is possible for
|
||||
// the notification functions to be called multiple times, so we guard them
|
||||
// with these booleans
|
||||
_notified_fail: bool,
|
||||
_notified_start: bool,
|
||||
_notified_complete: bool,
|
||||
|
||||
@@ -414,6 +427,7 @@ pub const Request = struct {
|
||||
._keepalive = false,
|
||||
._redirect_count = 0,
|
||||
._has_host_header = false,
|
||||
._notified_fail = false,
|
||||
._notified_start = false,
|
||||
._notified_complete = false,
|
||||
._connection_from_keepalive = false,
|
||||
@@ -428,6 +442,7 @@ pub const Request = struct {
|
||||
}
|
||||
|
||||
pub fn abort(self: *Request) void {
|
||||
self.requestFailed("aborted");
|
||||
const aborter = self._aborter orelse {
|
||||
self.deinit();
|
||||
return;
|
||||
@@ -555,6 +570,10 @@ pub const Request = struct {
|
||||
}
|
||||
|
||||
fn doSendSync(self: *Request, use_pool: bool) anyerror!Response {
|
||||
// https://github.com/ziglang/zig/issues/20369
|
||||
// errdefer |err| self.requestFailed(@errorName(err));
|
||||
errdefer self.requestFailed("network error");
|
||||
|
||||
if (use_pool) {
|
||||
if (self.findExistingConnection(true)) |connection| {
|
||||
self._connection = connection;
|
||||
@@ -847,6 +866,19 @@ pub const Request = struct {
|
||||
});
|
||||
}
|
||||
|
||||
fn requestFailed(self: *Request, err: []const u8) void {
|
||||
const notification = self.notification orelse return;
|
||||
if (self._notified_fail) {
|
||||
return;
|
||||
}
|
||||
self._notified_fail = true;
|
||||
notification.dispatch(.http_request_fail, &.{
|
||||
.id = self.id,
|
||||
.err = err,
|
||||
.url = self.request_uri,
|
||||
});
|
||||
}
|
||||
|
||||
fn requestCompleted(self: *Request, response: ResponseHeader) void {
|
||||
const notification = self.notification orelse return;
|
||||
if (self._notified_complete) {
|
||||
@@ -1290,6 +1322,8 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
|
||||
self.handler.onHttpResponse(err) catch {};
|
||||
// just to be safe
|
||||
self.request._keepalive = false;
|
||||
|
||||
self.request.requestFailed(@errorName(err));
|
||||
self.request.deinit();
|
||||
}
|
||||
|
||||
@@ -2509,6 +2543,12 @@ const StatePool = struct {
|
||||
allocator.free(self.states);
|
||||
}
|
||||
|
||||
pub fn freeSlotCount(self: *StatePool) usize {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
return self.available;
|
||||
}
|
||||
|
||||
pub fn acquireWait(self: *StatePool) *State {
|
||||
const states = self.states;
|
||||
|
||||
@@ -3000,8 +3040,14 @@ test "HttpClient: async connect error" {
|
||||
.{},
|
||||
);
|
||||
|
||||
try loop.io.run_for_ns(std.time.ns_per_ms);
|
||||
try reset.timedWait(std.time.ns_per_s);
|
||||
for (0..10) |_| {
|
||||
try loop.io.run_for_ns(std.time.ns_per_ms * 10);
|
||||
if (reset.isSet()) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
return error.Timeout;
|
||||
}
|
||||
}
|
||||
|
||||
test "HttpClient: async no body" {
|
||||
@@ -3124,7 +3170,7 @@ test "HttpClient: async tls no body" {
|
||||
}
|
||||
}
|
||||
|
||||
test "HttpClient: async tls with body x" {
|
||||
test "HttpClient: async tls with body" {
|
||||
defer testing.reset();
|
||||
for (0..5) |_| {
|
||||
var client = try testClient();
|
||||
|
||||
29
src/log.zig
29
src/log.zig
@@ -146,6 +146,16 @@ fn logTo(comptime scope: Scope, level: Level, comptime msg: []const u8, data: an
|
||||
}
|
||||
|
||||
fn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
|
||||
try logLogFmtPrefix(scope, level, msg, writer);
|
||||
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
|
||||
const key = " " ++ f.name ++ "=";
|
||||
try writer.writeAll(key);
|
||||
try writeValue(.logfmt, @field(data, f.name), writer);
|
||||
}
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
fn logLogFmtPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: anytype) !void {
|
||||
try writer.writeAll("$time=");
|
||||
try writer.print("{d}", .{timestamp()});
|
||||
|
||||
@@ -164,15 +174,20 @@ fn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data
|
||||
break :blk prefix ++ "\"" ++ msg ++ "\"";
|
||||
};
|
||||
try writer.writeAll(full_msg);
|
||||
}
|
||||
|
||||
fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
|
||||
try logPrettyPrefix(scope, level, msg, writer);
|
||||
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
|
||||
const key = " " ++ f.name ++ "=";
|
||||
const key = " " ++ f.name ++ " = ";
|
||||
try writer.writeAll(key);
|
||||
try writeValue(.logfmt, @field(data, f.name), writer);
|
||||
try writeValue(.pretty, @field(data, f.name), writer);
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
|
||||
fn logPrettyPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: anytype) !void {
|
||||
if (scope == .console and level == .fatal and comptime std.mem.eql(u8, msg, "lightpanda")) {
|
||||
try writer.writeAll("\x1b[0;104mWARN ");
|
||||
} else {
|
||||
@@ -201,14 +216,6 @@ fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data
|
||||
try writer.print(" \x1b[0m[+{d}ms]", .{elapsed()});
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
|
||||
const key = " " ++ f.name ++ " = ";
|
||||
try writer.writeAll(key);
|
||||
try writeValue(.pretty, @field(data, f.name), writer);
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
pub fn writeValue(comptime format: Format, value: anytype, writer: anytype) !void {
|
||||
|
||||
@@ -113,7 +113,7 @@ fn run(arena: Allocator, test_file: []const u8, loader: *FileLoader, err_out: *?
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
try polyfill.load(arena, runner.page.scope);
|
||||
try polyfill.load(arena, runner.page.main_context);
|
||||
|
||||
// loop over the scripts.
|
||||
const doc = parser.documentHTMLToDocument(runner.page.window.document);
|
||||
@@ -155,7 +155,7 @@ fn run(arena: Allocator, test_file: []const u8, loader: *FileLoader, err_out: *?
|
||||
{
|
||||
// wait for all async executions
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(runner.page.scope);
|
||||
try_catch.init(runner.page.main_context);
|
||||
defer try_catch.deinit();
|
||||
try runner.page.loop.run();
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ pub const Notification = struct {
|
||||
page_created: List = .{},
|
||||
page_navigate: List = .{},
|
||||
page_navigated: List = .{},
|
||||
http_request_fail: List = .{},
|
||||
http_request_start: List = .{},
|
||||
http_request_complete: List = .{},
|
||||
notification_created: List = .{},
|
||||
@@ -69,6 +70,7 @@ pub const Notification = struct {
|
||||
page_created: *page.Page,
|
||||
page_navigate: *const PageNavigate,
|
||||
page_navigated: *const PageNavigated,
|
||||
http_request_fail: *const RequestFail,
|
||||
http_request_start: *const RequestStart,
|
||||
http_request_complete: *const RequestComplete,
|
||||
notification_created: *Notification,
|
||||
@@ -97,6 +99,12 @@ pub const Notification = struct {
|
||||
has_body: bool,
|
||||
};
|
||||
|
||||
pub const RequestFail = struct {
|
||||
id: usize,
|
||||
url: *const std.Uri,
|
||||
err: []const u8,
|
||||
};
|
||||
|
||||
pub const RequestComplete = struct {
|
||||
id: usize,
|
||||
url: *const std.Uri,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -81,12 +81,13 @@ pub const Loop = struct {
|
||||
|
||||
// run tail events. We do run the tail events to ensure all the
|
||||
// contexts are correcly free.
|
||||
while (self.hasPendinEvents()) {
|
||||
self.io.run_for_ns(10 * std.time.ns_per_ms) catch |err| {
|
||||
while (self.pending_network_count != 0 or self.pending_timeout_count != 0) {
|
||||
self.io.run_for_ns(std.time.ns_per_ms * 10) catch |err| {
|
||||
log.err(.loop, "deinit", .{ .err = err });
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
if (comptime CANCEL_SUPPORTED) {
|
||||
self.io.cancel_all();
|
||||
}
|
||||
@@ -96,21 +97,6 @@ pub const Loop = struct {
|
||||
self.cancelled.deinit(self.alloc);
|
||||
}
|
||||
|
||||
// We can shutdown once all the pending network IO is complete.
|
||||
// In debug mode we also wait until al the pending timeouts are complete
|
||||
// but we only do this so that the `timeoutCallback` can free all allocated
|
||||
// memory and we won't report a leak.
|
||||
fn hasPendinEvents(self: *const Self) bool {
|
||||
if (self.pending_network_count > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (builtin.mode != .Debug) {
|
||||
return false;
|
||||
}
|
||||
return self.pending_timeout_count > 0;
|
||||
}
|
||||
|
||||
// Retrieve all registred I/O events completed by OS kernel,
|
||||
// and execute sequentially their callbacks.
|
||||
// Stops when there is no more I/O events registered on the loop.
|
||||
@@ -121,13 +107,14 @@ pub const Loop = struct {
|
||||
self.stopping = true;
|
||||
defer self.stopping = false;
|
||||
|
||||
while (self.pending_network_count > 0) {
|
||||
try self.io.run_for_ns(10 * std.time.ns_per_ms);
|
||||
// at each iteration we might have new events registred by previous callbacks
|
||||
while (self.pending_network_count != 0 or self.pending_timeout_count != 0) {
|
||||
self.io.run_for_ns(std.time.ns_per_ms * 10) catch |err| {
|
||||
log.err(.loop, "deinit", .{ .err = err });
|
||||
break;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// JS callbacks APIs
|
||||
// -----------------
|
||||
|
||||
@@ -255,7 +242,6 @@ pub const Loop = struct {
|
||||
}
|
||||
}.onConnect;
|
||||
|
||||
|
||||
const callback = try self.event_callback_pool.create();
|
||||
errdefer self.event_callback_pool.destroy(callback);
|
||||
callback.* = .{ .loop = self, .ctx = ctx };
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
|
||||
|
||||
return struct {
|
||||
env: *Env,
|
||||
scope: *Env.Scope,
|
||||
js_context: *Env.JsContext,
|
||||
executor: Env.ExecutionWorld,
|
||||
|
||||
pub const Env = js.Env(State, struct {
|
||||
@@ -48,7 +48,7 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
|
||||
self.executor = try self.env.newExecutionWorld();
|
||||
errdefer self.executor.deinit();
|
||||
|
||||
self.scope = try self.executor.startScope(
|
||||
self.js_context = try self.executor.createJsContext(
|
||||
if (Global == void) &default_global else global,
|
||||
state,
|
||||
{},
|
||||
@@ -68,10 +68,10 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
|
||||
pub fn testCases(self: *Self, cases: []const Case, _: RunOpts) !void {
|
||||
for (cases, 0..) |case, i| {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(self.scope);
|
||||
try_catch.init(self.js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const value = self.scope.exec(case.@"0", null) catch |err| {
|
||||
const value = self.js_context.exec(case.@"0", null) catch |err| {
|
||||
if (try try_catch.err(allocator)) |msg| {
|
||||
defer allocator.free(msg);
|
||||
if (isExpectedTypeError(case.@"1", msg)) {
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
// Copyright (C) 2023-2024 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/>.
|
||||
|
||||
// some utils to parser strings.
|
||||
const std = @import("std");
|
||||
|
||||
pub const Reader = struct {
|
||||
pos: usize = 0,
|
||||
data: []const u8,
|
||||
|
||||
pub fn until(self: *Reader, c: u8) []const u8 {
|
||||
const pos = self.pos;
|
||||
const data = self.data;
|
||||
|
||||
const index = std.mem.indexOfScalarPos(u8, data, pos, c) orelse data.len;
|
||||
self.pos = index;
|
||||
return data[pos..index];
|
||||
}
|
||||
|
||||
pub fn tail(self: *Reader) []const u8 {
|
||||
const pos = self.pos;
|
||||
const data = self.data;
|
||||
if (pos > data.len) {
|
||||
return "";
|
||||
}
|
||||
self.pos = data.len;
|
||||
return data[pos..];
|
||||
}
|
||||
|
||||
pub fn skip(self: *Reader) bool {
|
||||
const pos = self.pos;
|
||||
if (pos >= self.data.len) {
|
||||
return false;
|
||||
}
|
||||
self.pos = pos + 1;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// converts a comptime-known string (i.e. null terminated) to an uint
|
||||
pub fn asUint(comptime string: anytype) AsUintReturn(string) {
|
||||
const byteLength = @bitSizeOf(@TypeOf(string.*)) / 8 - 1;
|
||||
const expectedType = *const [byteLength:0]u8;
|
||||
if (@TypeOf(string) != expectedType) {
|
||||
@compileError("expected : " ++ @typeName(expectedType) ++
|
||||
", got: " ++ @typeName(@TypeOf(string)));
|
||||
}
|
||||
|
||||
return @bitCast(@as(*const [byteLength]u8, string).*);
|
||||
}
|
||||
|
||||
fn AsUintReturn(comptime string: anytype) type {
|
||||
return @Type(.{
|
||||
.int = .{
|
||||
.bits = @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
|
||||
.signedness = .unsigned,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const testing = std.testing;
|
||||
test "parser.Reader: skip" {
|
||||
var r = Reader{ .data = "foo" };
|
||||
try testing.expectEqual(true, r.skip());
|
||||
try testing.expectEqual(true, r.skip());
|
||||
try testing.expectEqual(true, r.skip());
|
||||
try testing.expectEqual(false, r.skip());
|
||||
try testing.expectEqual(false, r.skip());
|
||||
}
|
||||
|
||||
test "parser.Reader: tail" {
|
||||
var r = Reader{ .data = "foo" };
|
||||
try testing.expectEqualStrings("foo", r.tail());
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
}
|
||||
|
||||
test "parser.Reader: until" {
|
||||
var r = Reader{ .data = "foo.bar.baz" };
|
||||
try testing.expectEqualStrings("foo", r.until('.'));
|
||||
_ = r.skip();
|
||||
try testing.expectEqualStrings("bar", r.until('.'));
|
||||
_ = r.skip();
|
||||
try testing.expectEqualStrings("baz", r.until('.'));
|
||||
|
||||
r = Reader{ .data = "foo" };
|
||||
try testing.expectEqualStrings("foo", r.until('.'));
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
|
||||
r = Reader{ .data = "" };
|
||||
try testing.expectEqualStrings("", r.until('.'));
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
}
|
||||
|
||||
test "parser: asUint" {
|
||||
const ASCII_x = @as(u8, @bitCast([1]u8{'x'}));
|
||||
const ASCII_ab = @as(u16, @bitCast([2]u8{ 'a', 'b' }));
|
||||
const ASCII_xyz = @as(u24, @bitCast([3]u8{ 'x', 'y', 'z' }));
|
||||
const ASCII_abcd = @as(u32, @bitCast([4]u8{ 'a', 'b', 'c', 'd' }));
|
||||
|
||||
try testing.expectEqual(ASCII_x, asUint("x"));
|
||||
try testing.expectEqual(ASCII_ab, asUint("ab"));
|
||||
try testing.expectEqual(ASCII_xyz, asUint("xyz"));
|
||||
try testing.expectEqual(ASCII_abcd, asUint("abcd"));
|
||||
|
||||
try testing.expectEqual(u8, @TypeOf(asUint("x")));
|
||||
try testing.expectEqual(u16, @TypeOf(asUint("ab")));
|
||||
try testing.expectEqual(u24, @TypeOf(asUint("xyz")));
|
||||
try testing.expectEqual(u32, @TypeOf(asUint("abcd")));
|
||||
}
|
||||
@@ -419,17 +419,17 @@ pub const JsRunner = struct {
|
||||
const RunOpts = struct {};
|
||||
pub const Case = std.meta.Tuple(&.{ []const u8, ?[]const u8 });
|
||||
pub fn testCases(self: *JsRunner, cases: []const Case, _: RunOpts) !void {
|
||||
const scope = self.page.scope;
|
||||
const js_context = self.page.main_context;
|
||||
const arena = self.page.arena;
|
||||
|
||||
const start = try std.time.Instant.now();
|
||||
|
||||
for (cases, 0..) |case, i| {
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(scope);
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const value = scope.exec(case.@"0", null) catch |err| {
|
||||
const value = js_context.exec(case.@"0", null) catch |err| {
|
||||
if (try try_catch.err(arena)) |msg| {
|
||||
std.debug.print("{s}\n\nCase: {d}\n{s}\n", .{ msg, i + 1, case.@"0" });
|
||||
}
|
||||
@@ -453,14 +453,14 @@ pub const JsRunner = struct {
|
||||
}
|
||||
|
||||
pub fn eval(self: *JsRunner, src: []const u8, name: ?[]const u8, err_msg: *?[]const u8) !Env.Value {
|
||||
const scope = self.page.scope;
|
||||
const js_context = self.page.main_context;
|
||||
const arena = self.page.arena;
|
||||
|
||||
var try_catch: Env.TryCatch = undefined;
|
||||
try_catch.init(scope);
|
||||
try_catch.init(js_context);
|
||||
defer try_catch.deinit();
|
||||
|
||||
return scope.exec(src, name) catch |err| {
|
||||
return js_context.exec(src, name) catch |err| {
|
||||
if (try try_catch.err(arena)) |msg| {
|
||||
err_msg.* = msg;
|
||||
std.debug.print("Error running script: {s}\n", .{msg});
|
||||
|
||||
2
vendor/netsurf/libdom
vendored
2
vendor/netsurf/libdom
vendored
Submodule vendor/netsurf/libdom updated: 614187b0aa...73d1bc9f23
Reference in New Issue
Block a user