3 Commits

Author SHA1 Message Date
sjorsdonkers
6ec0d0b84c HtmlInputElement as Zig native 2025-06-16 13:29:28 +02:00
sjorsdonkers
3544e98871 create_element_external WIP 2025-06-16 13:29:27 +02:00
sjorsdonkers
446e5b2ddd basic idea 2025-06-16 13:29:27 +02:00
44 changed files with 691 additions and 2402 deletions

View File

@@ -43,6 +43,7 @@ const Matcher = struct {
}
};
const Elements = @import("../html/elements.zig");
test "matchFirst" {
const alloc = std.testing.allocator;
@@ -161,7 +162,7 @@ test "matchFirst" {
for (testcases) |tc| {
matcher.reset();
const doc = try parser.documentHTMLParseFromStr(tc.html);
const doc = try parser.documentHTMLParseFromStr(tc.html, &Elements.createElement);
defer parser.documentHTMLClose(doc) catch {};
const s = css.parse(alloc, tc.q, .{}) catch |e| {
@@ -302,7 +303,7 @@ test "matchAll" {
for (testcases) |tc| {
matcher.reset();
const doc = try parser.documentHTMLParseFromStr(tc.html);
const doc = try parser.documentHTMLParseFromStr(tc.html, &Elements.createElement);
defer parser.documentHTMLClose(doc) catch {};
const s = css.parse(alloc, tc.q, .{}) catch |e| {

View File

@@ -605,7 +605,6 @@ 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 },
}
}

View File

@@ -98,7 +98,6 @@ pub const PseudoClass = enum {
placeholder,
selection,
spelling_error,
modal,
pub const Error = error{
InvalidPseudoClass,
@@ -155,7 +154,6 @@ 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;
}
};

View File

@@ -1,42 +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/>.
const std = @import("std");
const CSSStyleSheet = @import("css_stylesheet.zig").CSSStyleSheet;
pub const Interfaces = .{
CSSRule,
CSSImportRule,
};
// https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
pub const CSSRule = struct {
css_text: []const u8,
parent_rule: ?*CSSRule = null,
parent_stylesheet: ?*CSSStyleSheet = null,
};
pub const CSSImportRule = struct {
pub const prototype = *CSSRule;
href: []const u8,
layer_name: ?[]const u8,
media: void,
style_sheet: CSSStyleSheet,
supports_text: ?[]const u8,
};

View File

@@ -1,60 +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/>.
const std = @import("std");
const StyleSheet = @import("stylesheet.zig").StyleSheet;
const CSSRule = @import("css_rule.zig").CSSRule;
const CSSImportRule = @import("css_rule.zig").CSSImportRule;
pub const CSSRuleList = struct {
list: std.ArrayListUnmanaged([]const u8),
pub fn constructor() CSSRuleList {
return .{ .list = .empty };
}
pub fn _item(self: *CSSRuleList, _index: u32) ?CSSRule {
const index: usize = @intCast(_index);
if (index > self.list.items.len) {
return null;
}
// todo: for now, just return null.
// this depends on properly parsing CSSRule
return null;
}
pub fn get_length(self: *CSSRuleList) u32 {
return @intCast(self.list.items.len);
}
};
const testing = @import("../../testing.zig");
test "Browser.CSS.CSSRuleList" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let list = new CSSRuleList()", "undefined" },
.{ "list instanceof CSSRuleList", "true" },
.{ "list.length", "0" },
.{ "list.item(0)", "null" },
}, .{});
}

View File

@@ -20,9 +20,15 @@ const std = @import("std");
const CSSParser = @import("./css_parser.zig").CSSParser;
const CSSValueAnalyzer = @import("./css_value_analyzer.zig").CSSValueAnalyzer;
const CSSRule = @import("css_rule.zig").CSSRule;
const Page = @import("../page.zig").Page;
pub const Interfaces = .{
CSSStyleDeclaration,
CSSRule,
};
const CSSRule = struct {};
pub const CSSStyleDeclaration = struct {
store: std.StringHashMapUnmanaged(Property),
order: std.ArrayListUnmanaged([]const u8),
@@ -79,7 +85,7 @@ pub const CSSStyleDeclaration = struct {
return self.order.items.len;
}
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
pub fn get_parentRule() ?CSSRule {
return null;
}

View File

@@ -1,91 +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/>.
const std = @import("std");
const Page = @import("../page.zig").Page;
const StyleSheet = @import("stylesheet.zig").StyleSheet;
const CSSRuleList = @import("css_rule_list.zig").CSSRuleList;
const CSSImportRule = @import("css_rule.zig").CSSImportRule;
pub const CSSStyleSheet = struct {
pub const prototype = *StyleSheet;
proto: StyleSheet,
css_rules: CSSRuleList,
owner_rule: ?*CSSImportRule,
const CSSStyleSheetOpts = struct {
base_url: ?[]const u8 = null,
// TODO: Suupport media
disabled: bool = false,
};
pub fn constructor(_opts: ?CSSStyleSheetOpts) !CSSStyleSheet {
const opts = _opts orelse CSSStyleSheetOpts{};
return .{
.proto = StyleSheet{ .disabled = opts.disabled },
.css_rules = .constructor(),
.owner_rule = null,
};
}
pub fn get_ownerRule(_: *CSSStyleSheet) ?*CSSImportRule {
return null;
}
pub fn get_cssRules(self: *CSSStyleSheet) *CSSRuleList {
return &self.css_rules;
}
pub fn _insertRule(self: *CSSStyleSheet, rule: []const u8, _index: ?usize, page: *Page) !usize {
const index = _index orelse 0;
if (index > self.css_rules.list.items.len) {
return error.IndexSize;
}
const arena = page.arena;
try self.css_rules.list.insert(arena, index, try arena.dupe(u8, rule));
return index;
}
pub fn _deleteRule(self: *CSSStyleSheet, index: usize) !void {
if (index > self.css_rules.list.items.len) {
return error.IndexSize;
}
_ = self.css_rules.list.orderedRemove(index);
}
};
const testing = @import("../../testing.zig");
test "Browser.CSS.StyleSheet" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let css = new CSSStyleSheet()", "undefined" },
.{ "css instanceof CSSStyleSheet", "true" },
.{ "css.cssRules.length", "0" },
.{ "css.ownerRule", "null" },
.{ "let index1 = css.insertRule('body { color: red; }', 0)", "undefined" },
.{ "index1", "0" },
.{ "css.cssRules.length", "1" },
}, .{});
}

View File

@@ -1,30 +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/>.
pub const Stylesheet = @import("stylesheet.zig").StyleSheet;
pub const CSSStylesheet = @import("css_stylesheet.zig").CSSStyleSheet;
pub const CSSStyleDeclaration = @import("css_style_declaration.zig").CSSStyleDeclaration;
pub const CSSRuleList = @import("css_rule_list.zig").CSSRuleList;
pub const Interfaces = .{
Stylesheet,
CSSStylesheet,
CSSStyleDeclaration,
CSSRuleList,
@import("css_rule.zig").Interfaces,
};

View File

@@ -1,55 +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/>.
const parser = @import("../netsurf.zig");
// https://developer.mozilla.org/en-US/docs/Web/API/StyleSheet#specifications
pub const StyleSheet = struct {
disabled: bool = false,
href: []const u8 = "",
owner_node: ?*parser.Node = null,
parent_stylesheet: ?*StyleSheet = null,
title: []const u8 = "",
type: []const u8 = "text/css",
pub fn get_disabled(self: *const StyleSheet) bool {
return self.disabled;
}
pub fn get_href(self: *const StyleSheet) []const u8 {
return self.href;
}
// TODO: media
pub fn get_ownerNode(self: *const StyleSheet) ?*parser.Node {
return self.owner_node;
}
pub fn get_parentStyleSheet(self: *const StyleSheet) ?*StyleSheet {
return self.parent_stylesheet;
}
pub fn get_title(self: *const StyleSheet) []const u8 {
return self.title;
}
pub fn get_type(self: *const StyleSheet) []const u8 {
return self.type;
}
};

View File

@@ -18,7 +18,6 @@
const std = @import("std");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
@@ -31,6 +30,7 @@ const css = @import("css.zig");
const Element = @import("element.zig").Element;
const ElementUnion = @import("element.zig").Union;
const Elements = @import("../html/elements.zig");
const TreeWalker = @import("tree_walker.zig").TreeWalker;
const Env = @import("../env.zig").Env;
@@ -46,6 +46,7 @@ pub const Document = struct {
pub fn constructor(page: *const Page) !*parser.DocumentHTML {
const doc = try parser.documentCreateDocument(
try parser.documentHTMLGetTitle(page.window.document),
&Elements.createElement,
);
// we have to work w/ document instead of html document.
@@ -121,28 +122,9 @@ pub const Document = struct {
return try Element.toInterface(e);
}
const CreateElementResult = union(enum) {
element: ElementUnion,
custom: Env.JsObject,
};
pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !CreateElementResult {
const custom_element = page.window.custom_elements._get(tag_name) orelse {
const e = try parser.documentCreateElement(self, tag_name);
return .{ .element = try Element.toInterface(e) };
};
var result: Env.Function.Result = undefined;
const js_obj = custom_element.newInstance(&result) catch |err| {
log.fatal(.user_script, "newInstance error", .{
.err = result.exception,
.stack = result.stack,
.tag_name = tag_name,
.source = "createElement",
});
return err;
};
return .{ .custom = js_obj };
pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
const e = try parser.documentCreateElement(self, tag_name);
return try Element.toInterface(e);
}
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {

View File

@@ -28,8 +28,6 @@ const IntersectionObserver = @import("intersection_observer.zig");
const DOMParser = @import("dom_parser.zig").DOMParser;
const TreeWalker = @import("tree_walker.zig").TreeWalker;
const NodeFilter = @import("node_filter.zig").NodeFilter;
const Performance = @import("performance.zig").Performance;
const PerformanceObserver = @import("performance_observer.zig").PerformanceObserver;
pub const Interfaces = .{
DOMException,
@@ -46,6 +44,4 @@ pub const Interfaces = .{
DOMParser,
TreeWalker,
NodeFilter,
Performance,
PerformanceObserver,
};

View File

@@ -30,8 +30,8 @@ pub const DOMParser = struct {
// TODO: Support XML
return error.TypeError;
}
return try parser.documentHTMLParseFromStr(string);
const Elements = @import("../html/elements.zig");
return try parser.documentHTMLParseFromStr(string, &Elements.createElement);
}
};

View File

@@ -43,10 +43,6 @@ pub const Element = struct {
y: f64,
width: f64,
height: f64,
bottom: f64,
right: f64,
top: f64,
left: f64,
};
pub fn toInterface(e: *parser.Element) !Union {
@@ -373,16 +369,7 @@ pub const Element = struct {
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect {
// Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes.
if (!try page.isNodeAttached(parser.elementToNode(self))) {
return DOMRect{
.x = 0,
.y = 0,
.width = 0,
.height = 0,
.bottom = 0,
.right = 0,
.top = 0,
.left = 0,
};
return DOMRect{ .x = 0, .y = 0, .width = 0, .height = 0 };
}
return page.renderer.getRect(self);
}

View File

@@ -42,7 +42,8 @@ pub const DOMImplementation = struct {
}
pub fn _createHTMLDocument(_: *DOMImplementation, title: ?[]const u8) !*parser.DocumentHTML {
return try parser.domImplementationCreateHTMLDocument(title);
const Elements = @import("../html/elements.zig");
return try parser.domImplementationCreateHTMLDocument(title, &Elements.createElement);
}
pub fn _hasFeature(_: *DOMImplementation) bool {

View File

@@ -1,34 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver
pub const PerformanceObserver = struct {
pub const _supportedEntryTypes = [0][]const u8{};
};
const testing = @import("../../testing.zig");
test "Browser.DOM.PerformanceObserver" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "PerformanceObserver.supportedEntryTypes.length", "0" },
}, .{});
}

View File

@@ -182,7 +182,7 @@ fn writeEscapedAttributeValue(writer: anytype, value: []const u8) !void {
const testing = std.testing;
test "dump.writeHTML" {
try parser.init();
try parser.init(testing.allocator);
defer parser.deinit();
try testWriteHTML(
@@ -225,7 +225,8 @@ fn testWriteFullHTML(comptime expected: []const u8, src: []const u8) !void {
var buf = std.ArrayListUnmanaged(u8){};
defer buf.deinit(testing.allocator);
const doc_html = try parser.documentHTMLParseFromStr(src);
const Elements = @import("html/elements.zig");
const doc_html = try parser.documentHTMLParseFromStr(src, &Elements.createElement);
defer parser.documentHTMLClose(doc_html) catch {};
const doc = parser.documentHTMLToDocument(doc_html);

View File

@@ -22,7 +22,7 @@ const WebApis = struct {
pub const Interfaces = generate.Tuple(.{
@import("crypto/crypto.zig").Crypto,
@import("console/console.zig").Console,
@import("cssom/cssom.zig").Interfaces,
@import("cssom/css_style_declaration.zig").Interfaces,
@import("dom/dom.zig").Interfaces,
@import("encoding/text_encoder.zig").Interfaces,
@import("events/event.zig").Interfaces,
@@ -33,7 +33,6 @@ const WebApis = struct {
@import("xhr/xhr.zig").Interfaces,
@import("xhr/form_data.zig").Interfaces,
@import("xmlserializer/xmlserializer.zig").Interfaces,
@import("webcomponents/webcomponents.zig").Interfaces,
});
};

View File

@@ -81,7 +81,7 @@ pub const HTMLDocument = struct {
pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 {
var buf: std.ArrayListUnmanaged(u8) = .{};
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true, .is_http = false });
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true });
return buf.items;
}
@@ -90,10 +90,6 @@ pub const HTMLDocument = struct {
// outlives the page's arena.
const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str);
errdefer c.deinit();
if (c.http_only) {
c.deinit();
return ""; // HttpOnly cookies cannot be set from JS
}
try page.cookie_jar.add(c, std.time.timestamp());
return cookie_str;
}
@@ -337,8 +333,6 @@ test "Browser.HTML.Document" {
.{ "document.cookie = 'name=Oeschger; SameSite=None; Secure'", "name=Oeschger; SameSite=None; Secure" },
.{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" },
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
.{ "document.cookie = 'IgnoreMy=Ghost; HttpOnly'", null }, // "" should be returned, but the framework overrules it atm
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
}, .{});
try runner.testCases(&.{

View File

@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const generate = @import("../../runtime/generate.zig");
@@ -27,6 +26,7 @@ const urlStitch = @import("../../url.zig").URL.stitch;
const URL = @import("../url/url.zig").URL;
const Node = @import("../dom/node.zig").Node;
const Element = @import("../dom/element.zig").Element;
const State = @import("../State.zig");
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
@@ -113,10 +113,6 @@ 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;
@@ -179,10 +175,6 @@ 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
@@ -192,10 +184,6 @@ 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
@@ -204,10 +192,6 @@ 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);
}
@@ -445,240 +429,144 @@ 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 {
@@ -686,10 +574,6 @@ 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);
}
@@ -730,7 +614,6 @@ 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;
@@ -744,13 +627,83 @@ pub const HTMLImageElement = struct {
};
};
pub fn createElement(params: [*c]parser.c.dom_html_element_create_params, elem: [*c][*c]parser.ElementHTML) callconv(.c) parser.c.dom_exception {
const p: *parser.c.dom_html_element_create_params = @ptrCast(params);
switch (p.type) {
parser.c.DOM_HTML_ELEMENT_TYPE_INPUT => {
return HTMLInputElement.dom_create(params, elem);
},
else => return parser.c.DOM_NO_ERR,
}
}
var input_protected_vtable: parser.c.dom_element_protected_vtable = .{
.base = .{
.destroy = HTMLInputElement.node_destroy,
.copy = HTMLInputElement.node_copy,
},
.dom_element_parse_attribute = HTMLInputElement.element_parse_attribute,
};
pub const HTMLInputElement = struct {
pub const Self = parser.Input;
pub const prototype = *HTMLElement;
pub const subtype = .node;
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
return constructHtmlElement(page, js_this);
base: parser.ElementHTML,
type: []const u8 = "text",
pub fn dom_create(params: *parser.c.dom_html_element_create_params, output: *?*parser.ElementHTML) parser.c.dom_exception {
var self = parser.ARENA.?.create(HTMLInputElement) catch return parser.c.DOM_NO_MEM_ERR;
output.* = &self.base; // Self can be recovered using @fieldParentPtr
self.base.base.base.base.vtable = &parser.c._dom_html_element_vtable; // TODO replace get/setAttribute
self.base.base.base.vtable = &input_protected_vtable;
return self.dom_initialise(params);
}
// Initialise is separated from create such that the leaf type sets the vtable, then calls all the way up the protochain to init
pub fn dom_initialise(self: *HTMLInputElement, params: *parser.c.dom_html_element_create_params) parser.c.dom_exception {
return parser.c._dom_html_element_initialise(params, &self.base);
}
// This should always be the same and we should not have cleanup for new zig implementation, hopefully
pub fn node_destroy(node: [*c]parser.Node) callconv(.c) void {
const elem = parser.nodeToHtmlElement(node);
parser.c._dom_html_element_finalise(elem);
}
pub fn node_copy(old: [*c]parser.Node, new: [*c][*c]parser.Node) callconv(.c) parser.c.dom_exception {
const old_elem = parser.nodeToHtmlElement(old);
const self = @as(*HTMLInputElement, @fieldParentPtr("base", old_elem));
var copy = parser.ARENA.?.create(HTMLInputElement) catch return parser.c.DOM_NO_MEM_ERR;
copy.type = self.type;
const err = parser.c._dom_html_element_copy_internal(old_elem, &copy.base);
if (err != parser.c.DOM_NO_ERR) {
return err;
}
new.* = @ptrCast(copy);
return parser.c.DOM_NO_ERR;
}
// fn ([*c]cimport.struct_dom_element, [*c]cimport.struct_dom_string, [*c]cimport.struct_dom_string, [*c][*c]cimport.struct_dom_string) callconv(.c) c_uint
pub fn element_parse_attribute(self: [*c]parser.Element, name: [*c]parser.c.dom_string, value: [*c]parser.c.dom_string, parsed: [*c][*c]parser.c.dom_string) callconv(.c) parser.c.dom_exception {
_ = name;
_ = self;
parsed.* = value;
_ = parser.c.dom_string_ref(value);
// TODO actual implementation
// Probably should not use this and instead override the getAttribute setAttribute Element methods directly, perhaps other related functions.
// handle defaultValue likes
// Call setter or store in general attribute store
// increment domstring ref?
return parser.c.DOM_NO_ERR;
}
pub fn get_defaultValue(self: *parser.Input) ![]const u8 {
@@ -824,10 +777,26 @@ pub const HTMLInputElement = struct {
try parser.inputSetSrc(self, new_src);
}
pub fn get_type(self: *parser.Input) ![]const u8 {
return try parser.inputGetType(self);
const elem = parser.nodeToHtmlElement(@alignCast(@ptrCast(self)));
const input = @as(*HTMLInputElement, @fieldParentPtr("base", elem));
return input.type;
}
pub fn set_type(self: *parser.Input, type_: []const u8) !void {
try parser.inputSetType(self, type_);
const elem = parser.nodeToHtmlElement(@alignCast(@ptrCast(self)));
const input = @as(*HTMLInputElement, @fieldParentPtr("base", elem));
const possible_values = [_][]const u8{ "text", "search", "tel", "url", "email", "password", "date", "month", "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio", "file", "hidden", "image", "button", "submit", "reset" };
var found = false;
for (possible_values) |item| {
if (std.mem.eql(u8, type_, item)) {
found = true;
break;
}
}
input.type = if (found) type_ else "text";
// TODO DOM events
}
pub fn get_value(self: *parser.Input) ![]const u8 {
return try parser.inputGetValue(self);
@@ -841,190 +810,114 @@ 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
@@ -1033,10 +926,6 @@ 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),
@@ -1171,166 +1060,101 @@ 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)) },
@@ -1402,16 +1226,6 @@ 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, .{});

View File

@@ -24,6 +24,7 @@ const Navigator = @import("navigator.zig").Navigator;
const History = @import("history.zig").History;
const Location = @import("location.zig").Location;
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
const Performance = @import("performance.zig").Performance;
pub const Interfaces = .{
HTMLDocument,
@@ -36,5 +37,5 @@ pub const Interfaces = .{
History,
Location,
MediaQueryList,
@import("screen.zig").Interfaces,
Performance,
};

View File

@@ -1,109 +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/>.
const std = @import("std");
const EventTarget = @import("../dom/event_target.zig").EventTarget;
pub const Interfaces = .{
Screen,
ScreenOrientation,
};
// https://developer.mozilla.org/en-US/docs/Web/API/Screen
pub const Screen = struct {
pub const prototype = *EventTarget;
height: u32 = 1080,
width: u32 = 1920,
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/colorDepth
color_depth: u32 = 8,
// https://developer.mozilla.org/en-US/docs/Web/API/Screen/pixelDepth
pixel_depth: u32 = 8,
orientation: ScreenOrientation = .{ .type = .landscape_primary },
pub fn get_availHeight(self: *const Screen) u32 {
return self.height;
}
pub fn get_availWidth(self: *const Screen) u32 {
return self.width;
}
pub fn get_height(self: *const Screen) u32 {
return self.height;
}
pub fn get_width(self: *const Screen) u32 {
return self.width;
}
pub fn get_pixelDepth(self: *const Screen) u32 {
return self.pixel_depth;
}
pub fn get_orientation(self: *const Screen) ScreenOrientation {
return self.orientation;
}
};
const ScreenOrientationType = enum {
portrait_primary,
portrait_secondary,
landscape_primary,
landscape_secondary,
pub fn toString(self: ScreenOrientationType) []const u8 {
return switch (self) {
.portrait_primary => "portrait-primary",
.portrait_secondary => "portrait-secondary",
.landscape_primary => "landscape-primary",
.landscape_secondary => "landscape-secondary",
};
}
};
pub const ScreenOrientation = struct {
pub const prototype = *EventTarget;
angle: u32 = 0,
type: ScreenOrientationType,
pub fn get_angle(self: *const ScreenOrientation) u32 {
return self.angle;
}
pub fn get_type(self: *const ScreenOrientation) []const u8 {
return self.type.toString();
}
};
const testing = @import("../../testing.zig");
test "Browser.HTML.Screen" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "let screen = window.screen", "undefined" },
.{ "screen.width === 1920", "true" },
.{ "screen.height === 1080", "true" },
.{ "let orientation = screen.orientation", "undefined" },
.{ "orientation.angle === 0", "true" },
.{ "orientation.type === \"landscape-primary\"", "true" },
}, .{});
}

View File

@@ -31,10 +31,8 @@ const Crypto = @import("../crypto/crypto.zig").Crypto;
const Console = @import("../console/console.zig").Console;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
const Performance = @import("../dom/performance.zig").Performance;
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 Screen = @import("screen.zig").Screen;
const storage = @import("../storage/storage.zig");
@@ -60,12 +58,11 @@ pub const Window = struct {
console: Console = .{},
navigator: Navigator = .{},
performance: Performance,
custom_elements: CustomElementRegistry = .{},
screen: Screen = .{},
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
var fbs = std.io.fixedBufferStream("");
const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8");
const Elements = @import("../html/elements.zig");
const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8", &Elements.createElement);
const doc = parser.documentHTMLToDocument(html_doc);
try parser.documentSetDocumentURI(doc, "about:blank");
@@ -167,14 +164,6 @@ pub const Window = struct {
return &self.performance;
}
pub fn get_customElements(self: *Window) *CustomElementRegistry {
return &self.custom_elements;
}
pub fn get_screen(self: *Window) *Screen {
return &self.screen;
}
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
return self.createTimeout(cbk, 5, page, .{ .animation_frame = true });
}
@@ -349,15 +338,20 @@ 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 = 0;
\\ let start;
\\ function step(timestamp) {
\\ start = timestamp;
\\ if (start === undefined) {
\\ start = timestamp;
\\ }
\\ const elapsed = timestamp - start;
\\ if (elapsed < 2000) {
\\ requestAnimationFrame(step);
\\ }
\\ }
,
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
@@ -393,19 +387,4 @@ test "Browser.HTML.Window" {
.{ "window.setTimeout(() => {longCall = true}, 5001);", null },
.{ "longCall;", "false" },
}, .{});
// window event target
try runner.testCases(&.{
.{
\\ let called = false;
\\ window.addEventListener("ready", (e) => {
\\ called = (e.currentTarget == window);
\\ }, {capture: false, once: false});
\\ const evt = new Event("ready", { bubbles: true, cancelable: false });
\\ window.dispatchEvent(evt);
\\ called;
,
"true",
},
}, .{});
}

View File

@@ -1,337 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const parser = @import("netsurf.zig");
const Walker = @import("dom/walker.zig").WalkerChildren;
const URL = @import("../url.zig").URL;
const NP = "\n\n";
const Elem = struct {
inlin: bool = false,
list_order: ?u8 = null,
parent: ?*Elem = null,
};
const State = struct {
block: bool,
last_char: u8,
elem: ?*Elem = null,
fn is_inline(state: *State) bool {
if (state.elem == null) return false;
return state.elem.?.inlin;
}
fn last_char_space(state: *State) bool {
if (state.last_char == ' ' or state.last_char == '\n') return true;
return false;
}
};
// writer must be a std.io.Writer
pub fn writeMarkdown(url: URL, doc: *parser.Document, writer: anytype) !void {
var state = State{ .block = true, .last_char = '\n' };
_ = try writeChildren(url, parser.documentToNode(doc), &state, writer);
try writer.writeAll("\n");
}
fn writeChildren(url: URL, root: *parser.Node, state: *State, writer: anytype) !void {
const walker = Walker{};
var next: ?*parser.Node = null;
while (true) {
next = try walker.get_next(root, next) orelse break;
try writeNode(url, next.?, state, writer);
}
}
fn ensureBlock(state: *State, writer: anytype) !void {
if (state.is_inline()) return;
if (!state.block) {
try writer.writeAll(NP);
state.last_char = '\n';
state.block = true;
}
}
fn writeInline(state: *State, text: []const u8, writer: anytype) !void {
try writer.writeAll(text);
state.last_char = text[text.len - 1];
if (state.block) state.block = false;
}
const order = [_][]const u8{
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10",
"11", "12", "13", "14", "15", "16", "17", "18", "19", "20",
"21", "22", "23", "24", "25", "26", "27", "28", "29", "30",
"31", "32", "33", "34", "35", "36", "37", "38", "39", "40",
"41", "42", "43", "44", "45", "46", "47", "48", "49", "50",
};
fn writeNode(url: URL, node: *parser.Node, state: *State, writer: anytype) anyerror!void {
switch (try parser.nodeType(node)) {
.element => {
const html_element: *parser.ElementHTML = @ptrCast(node);
const tag = try parser.elementHTMLGetTagType(html_element);
// debug
// try writer.writeAll("\nstart - ");
// try writer.writeAll(@tagName(tag));
// try writer.writeAll("\n");
switch (tag) {
// skip element, go to children
.html, .head, .meta, .link, .body, .span => {
try writeChildren(url, node, state, writer);
},
// skip element and children
.title, .i, .script, .noscript, .undef, .style => {},
// generic elements
.h1, .h2, .h3, .h4, .h5, .h6 => {
try ensureBlock(state, writer);
if (!state.is_inline()) {
switch (tag) {
.h1 => try writeInline(state, "# ", writer),
.h2 => try writeInline(state, "## ", writer),
.h3 => try writeInline(state, "### ", writer),
.h4 => try writeInline(state, "#### ", writer),
.h5 => try writeInline(state, "##### ", writer),
.h6 => try writeInline(state, "###### ", writer),
else => @panic("only headers tags are supported here"),
}
}
try writeChildren(url, node, state, writer);
try ensureBlock(state, writer);
},
// containers and dividers
.header, .footer, .nav, .section, .div, .article, .p, .button, .form => {
try ensureBlock(state, writer);
try writeChildren(url, node, state, writer);
try ensureBlock(state, writer);
},
.br => {
try ensureBlock(state, writer);
try writeChildren(url, node, state, writer);
},
.hr => {
try ensureBlock(state, writer);
try writeInline(state, "---", writer);
try ensureBlock(state, writer);
},
// styling
.b => {
var elem = Elem{ .parent = state.elem, .inlin = true };
state.elem = &elem;
defer state.elem = elem.parent;
try writeInline(state, "**", writer);
try writeChildren(url, node, state, writer);
try writeInline(state, "**", writer);
},
// specific elements
.a => {
if (!state.last_char_space()) try writeInline(state, " ", writer);
var elem = Elem{ .parent = state.elem, .inlin = true };
state.elem = &elem;
defer state.elem = elem.parent;
const element = parser.nodeToElement(node);
if (try getAttributeValue(element, "href")) |href| {
try writeInline(state, "[", writer);
try writeChildren(url, node, state, writer);
try writeInline(state, "](", writer);
// handle relative path
if (href[0] == '/') {
try writeInline(state, url.scheme(), writer);
try writeInline(state, "://", writer);
try writeInline(state, url.host(), writer);
}
try writeInline(state, href, writer);
try writeInline(state, ")", writer);
} else {
try writeChildren(url, node, state, writer);
}
},
.img => {
var elem = Elem{ .parent = state.elem, .inlin = true };
state.elem = &elem;
defer state.elem = elem.parent;
const element = parser.nodeToElement(node);
if (try getAttributeValue(element, "src")) |src| {
try writeInline(state, "![", writer);
if (try getAttributeValue(element, "alt")) |alt| {
try writeInline(state, alt, writer);
} else {
try writeInline(state, src, writer);
}
try writeInline(state, "](", writer);
// handle relative path
if (src[0] == '/') {
try writeInline(state, url.scheme(), writer);
try writeInline(state, "://", writer);
try writeInline(state, url.host(), writer);
}
try writeInline(state, src, writer);
try writeInline(state, ")", writer);
}
},
.ul => {
var elem = Elem{ .parent = state.elem, .list_order = 0 };
state.elem = &elem;
defer state.elem = elem.parent;
try ensureBlock(state, writer);
try writeChildren(url, node, state, writer);
try ensureBlock(state, writer);
},
.ol => {
var elem = Elem{ .parent = state.elem, .list_order = 1 };
state.elem = &elem;
defer state.elem = elem.parent;
try ensureBlock(state, writer);
try writeChildren(url, node, state, writer);
try ensureBlock(state, writer);
},
.li => blk: {
const parent = state.elem orelse break :blk;
const list_order = parent.list_order orelse break :blk;
if (!state.block) try writer.writeAll("\n");
if (list_order > 0) {
// ordered list
try writeInline(state, order[list_order - 1], writer);
try writeInline(state, ". ", writer);
parent.list_order = list_order + 1;
} else {
// unordered list
try writeInline(state, "- ", writer);
}
try writeChildren(url, node, state, writer);
},
.input => {
var elem = Elem{ .parent = state.elem, .inlin = true };
state.elem = &elem;
defer state.elem = elem.parent;
const element = parser.nodeToElement(node);
if (try getAttributeValue(element, "value")) |value| {
try writeInline(state, value, writer);
try writeInline(state, " ", writer);
}
},
else => {
try ensureBlock(state, writer);
try writer.writeAll(@tagName(tag));
try writer.writeAll(" not supported");
try ensureBlock(state, writer);
},
}
// try writer.writeAll("\nend - ");
// try writer.writeAll(@tagName(tag));
// try writer.writeAll("\n");
},
.text => {
const v = try parser.nodeValue(node) orelse return;
const printed = try writeText(state, v, writer);
if (printed) state.block = false;
},
.cdata_section => {},
.comment => {},
// TODO handle processing instruction dump
.processing_instruction => {},
// document fragment is outside of the main document DOM, so we
// don't output it.
.document_fragment => {},
// document will never be called, but required for completeness.
.document => {},
// done globally instead, but required for completeness. Only the outer DOCTYPE should be written
.document_type => {},
// deprecated
.attribute, .entity_reference, .entity, .notation => {},
}
}
// TODO: not sure about + - . ! as they are very common characters
// I fear that we add too much escape strings
// TODO: | (pipe)
const escape = [_]u8{ '\\', '`', '*', '_', '{', '}', '[', ']', '<', '>', '(', ')', '#' };
fn writeText(state: *State, value: []const u8, writer: anytype) !bool {
if (value.len == 0) return false;
var last_char: u8 = ' ';
var printed: u64 = 0;
for (value, 0..) |v, i| {
// do not print:
// - multiple spaces
// - return line
// - tabs
if (v == last_char and v == ' ') continue;
if (v == '\n') continue;
if (v == '\t') continue;
// escape char
for (escape) |esc| {
if (v == esc) try writer.writeAll("\\");
}
if (printed == 0 and !state.is_inline()) {
if (state.last_char != '\n' and state.last_char != ' ') {
try writer.writeAll(" ");
}
}
last_char = v;
printed += 1;
const x = [_]u8{v}; // TODO: do we have something better?
try writer.writeAll(&x);
if (i == value.len - 1) state.last_char = v;
}
if (printed > 0) return true;
return false;
}
fn getAttributeValue(elem: *parser.Element, attr: []const u8) !?[]const u8 {
if (try parser.elementGetAttribute(elem, attr)) |value| {
if (value.len > 0) return value;
}
return null;
}
fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
var v = value;
while (v.len > 0) {
try writer.writeAll("TEXT: ");
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
return writer.writeAll(v);
};
try writer.writeAll(v[0..index]);
switch (v[index]) {
'&' => try writer.writeAll("&amp;"),
'<' => try writer.writeAll("&lt;"),
'>' => try writer.writeAll("&gt;"),
else => unreachable,
}
v = v[index + 1 ..];
}
}

View File

@@ -17,8 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const c = @cImport({
pub const c = @cImport({
@cInclude("dom/dom.h");
@cInclude("core/pi.h");
@cInclude("dom/bindings/hubbub/parser.h");
@@ -32,11 +33,13 @@ const c = @cImport({
});
const mimalloc = @import("mimalloc.zig");
pub var ARENA: ?Allocator = null;
// init initializes netsurf lib.
// init starts a mimalloc heap arena for the netsurf session. The caller must
// call deinit() to free the arena memory.
pub fn init() !void {
pub fn init(allocator: Allocator) !void {
ARENA = allocator;
try mimalloc.create();
}
@@ -49,6 +52,7 @@ pub fn deinit() void {
c.lwc_deinit_strings();
mimalloc.destroy();
ARENA = null;
}
// Vtable
@@ -775,17 +779,6 @@ pub const EventTargetTBase = extern struct {
.add_event_listener = add_event_listener,
.iter_event_listener = iter_event_listener,
},
// When we dispatch the event, we need to provide a target. In reality, the
// target is the container of this EventTargetTBase. But we can't pass that
// to _dom_event_target_dispatch, because it expects a dom_event_target.
// If you try to pass an non-event_target, you'll get weird behavior. For
// example, libdom might dom_node_ref that memory. Say we passed a *Window
// as the target, what happens if libdom calls dom_node_ref(window)? If
// you're lucky, you'll crash. If you're unlucky, you'll increment a random
// part of the window structure.
refcnt: u32 = 0,
eti: c.dom_event_target_internal = c.dom_event_target_internal{ .listeners = null },
pub fn add_event_listener(et: [*c]c.dom_event_target, t: [*c]c.dom_string, l: ?*c.struct_dom_event_listener, capture: bool) callconv(.C) c.dom_exception {
@@ -1368,6 +1361,10 @@ pub inline fn nodeToElement(node: *Node) *Element {
return @as(*Element, @ptrCast(node));
}
pub inline fn nodeToHtmlElement(node: *Node) *ElementHTML {
return @as(*ElementHTML, @alignCast(@ptrCast(node)));
}
// nodeToDocument is an helper to convert a node to an document.
pub inline fn nodeToDocument(node: *Node) *Document {
return @as(*Document, @ptrCast(node));
@@ -2000,8 +1997,10 @@ pub inline fn domImplementationCreateDocumentType(
return dt.?;
}
pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8) !*DocumentHTML {
const doc_html = try documentCreateDocument(title);
pub const CreateElementFn = ?*const fn ([*c]c.dom_html_element_create_params, [*c][*c]ElementHTML) callconv(.c) c.dom_exception;
pub inline fn domImplementationCreateHTMLDocument(title: ?[]const u8, create_element: CreateElementFn) !*DocumentHTML {
const doc_html = try documentCreateDocument(title, create_element);
const doc = documentHTMLToDocument(doc_html);
// add hierarchy: html, head, body.
@@ -2082,7 +2081,7 @@ pub inline fn documentSetInputEncoding(doc: *Document, enc: []const u8) !void {
try DOMErr(err);
}
pub inline fn documentCreateDocument(title: ?[]const u8) !*DocumentHTML {
pub inline fn documentCreateDocument(title: ?[]const u8, create_element: CreateElementFn) !*DocumentHTML {
var doc: ?*Document = undefined;
const err = c.dom_implementation_create_document(
c.DOM_IMPLEMENTATION_HTML,
@@ -2096,6 +2095,9 @@ pub inline fn documentCreateDocument(title: ?[]const u8) !*DocumentHTML {
try DOMErr(err);
const doc_html = @as(*DocumentHTML, @ptrCast(doc.?));
if (title) |t| try documentHTMLSetTitle(doc_html, t);
doc_html.create_element_external = create_element;
return doc_html;
}
@@ -2259,24 +2261,26 @@ fn parserErr(err: HubbubErr) ParserError!void {
// documentHTMLParseFromStr parses the given HTML string.
// The caller is responsible for closing the document.
pub fn documentHTMLParseFromStr(str: []const u8) !*DocumentHTML {
pub fn documentHTMLParseFromStr(str: []const u8, create_element: CreateElementFn) !*DocumentHTML {
var fbs = std.io.fixedBufferStream(str);
return try documentHTMLParse(fbs.reader(), "UTF-8");
return try documentHTMLParse(fbs.reader(), "UTF-8", create_element);
}
pub fn documentHTMLParse(reader: anytype, enc: ?[:0]const u8) !*DocumentHTML {
pub fn documentHTMLParse(reader: anytype, enc: ?[:0]const u8, create_element: CreateElementFn) !*DocumentHTML {
var parser: ?*c.dom_hubbub_parser = undefined;
var doc: ?*c.dom_document = undefined;
var err: c.hubbub_error = undefined;
var params = parseParams(enc);
err = c.dom_hubbub_parser_create(&params, &parser, &doc);
const result = @as(*DocumentHTML, @ptrCast(doc.?));
result.create_element_external = create_element;
try parserErr(err);
defer c.dom_hubbub_parser_destroy(parser);
try parseData(parser.?, reader);
return @as(*DocumentHTML, @ptrCast(doc.?));
return result;
}
pub fn documentParseFragmentFromStr(self: *Document, str: []const u8) !*DocumentFragment {

View File

@@ -22,7 +22,6 @@ const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const Dump = @import("dump.zig");
const Markdown = @import("markdown.zig");
const State = @import("State.zig");
const Env = @import("env.zig").Env;
const Mime = @import("mime.zig").Mime;
@@ -148,45 +147,20 @@ pub const Page = struct {
try Dump.writeHTML(doc, out);
}
// dump writes the page content into the given file.
pub fn markdown(self: *const Page, out: std.fs.File) !void {
if (self.raw_data) |_| {
// raw_data was set if the document was not HTML we can not convert it to Markdown,
return error.HTMLDocument;
}
// if the page has a pointer to a document, converts the HTML in Markdown and dump it.
const doc = parser.documentHTMLToDocument(self.window.document);
try Markdown.writeMarkdown(self.url, doc, out);
}
pub fn fetchModuleSource(ctx: *anyopaque, specifier: []const u8) !?[]const u8 {
const self: *Page = @ptrCast(@alignCast(ctx));
const base = if (self.current_script) |s| s.src else null;
const src = blk: {
const file_src = blk: {
if (base) |_base| {
break :blk try URL.stitch(self.arena, specifier, _base, .{});
} else break :blk specifier;
};
if (self.module_map.get(src)) |module| {
log.debug(.http, "fetching module", .{
.src = src,
.cached = true,
});
return module;
}
log.debug(.http, "fetching module", .{
.src = src,
.base = base,
.cached = false,
.specifier = specifier,
});
if (self.module_map.get(file_src)) |module| return module;
const module = try self.fetchData(specifier, base);
if (module) |_module| try self.module_map.putNoClobber(self.arena, src, _module);
if (module) |_module| try self.module_map.putNoClobber(self.arena, file_src, _module);
return module;
}
@@ -243,7 +217,7 @@ pub const Page = struct {
{
// block exists to limit the lifetime of the request, which holds
// onto a connection
var request = try self.newHTTPRequest(opts.method, &self.url, .{ .navigation = true, .is_http = true });
var request = try self.newHTTPRequest(opts.method, &self.url, .{ .navigation = true });
defer request.deinit();
request.body = opts.body;
@@ -312,7 +286,8 @@ pub const Page = struct {
pub fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
const ccharset = try self.arena.dupeZ(u8, charset);
const html_doc = try parser.documentHTMLParse(reader, ccharset);
const Elements = @import("html/elements.zig");
const html_doc = try parser.documentHTMLParse(reader, ccharset, &Elements.createElement);
const doc = parser.documentHTMLToDocument(html_doc);
// inject the URL to the document including the fragment.
@@ -378,23 +353,8 @@ pub const Page = struct {
continue;
}
const current = next.?;
const e = parser.nodeToElement(current);
const e = parser.nodeToElement(next.?);
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;
@@ -487,9 +447,9 @@ pub const Page = struct {
};
var script_source: ?[]const u8 = null;
defer self.current_script = 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 {
@@ -533,23 +493,12 @@ pub const Page = struct {
var origin_url = &self.url;
const url = try origin_url.resolve(arena, res_src);
var status_code: u16 = 0;
log.debug(.http, "fetching script", .{
.url = url,
.src = src,
.base = base,
});
errdefer |err| log.err(.http, "fetch error", .{
.err = err,
.url = url,
.status = status_code,
});
log.debug(.http, "fetching script", .{ .url = url });
errdefer |err| log.err(.http, "fetch error", .{ .err = err, .url = url });
var request = try self.newHTTPRequest(.GET, &url, .{
.origin_uri = &origin_url.uri,
.navigation = false,
.is_http = true,
});
defer request.deinit();
@@ -557,8 +506,7 @@ pub const Page = struct {
var header = response.header;
try self.session.cookie_jar.populateFromResponse(&url.uri, &header);
status_code = header.status;
if (status_code < 200 or status_code > 299) {
if (header.status < 200 or header.status > 299) {
return error.BadStatusCode;
}
@@ -576,7 +524,7 @@ pub const Page = struct {
log.info(.http, "fetch complete", .{
.url = url,
.status = status_code,
.status = header.status,
.content_length = arr.items.len,
});
return arr.items;
@@ -1039,9 +987,6 @@ const Script = struct {
defer try_catch.deinit();
const src = self.src orelse "inline";
log.debug(.browser, "executing script", .{ .src = src, .kind = self.kind });
_ = switch (self.kind) {
.javascript => page.main_context.exec(body, src),
.module => blk: {

View File

@@ -62,38 +62,20 @@ const FlatRenderer = struct {
gop.value_ptr.* = x;
}
const _x: f64 = @floatFromInt(x);
const y: f64 = 0.0;
const w: f64 = 1.0;
const h: f64 = 1.0;
return .{
.x = _x,
.y = y,
.width = w,
.height = h,
.left = _x,
.top = y,
.right = _x + w,
.bottom = y + h,
.x = @floatFromInt(x),
.y = 0.0,
.width = 1.0,
.height = 1.0,
};
}
pub fn boundingRect(self: *const FlatRenderer) Element.DOMRect {
const x: f64 = 0.0;
const y: f64 = 0.0;
const w: f64 = @floatFromInt(self.width());
const h: f64 = @floatFromInt(self.width());
return .{
.x = x,
.y = y,
.width = w,
.height = h,
.left = x,
.top = y,
.right = x + w,
.bottom = y + h,
.x = 0.0,
.y = 0.0,
.width = @floatFromInt(self.width()),
.height = @floatFromInt(self.height()),
};
}

View File

@@ -85,14 +85,14 @@ pub const Session = struct {
pub fn createPage(self: *Session) !*Page {
std.debug.assert(self.page == null);
// Start netsurf memory arena.
// We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded
try parser.init();
const page_arena = &self.browser.page_arena;
_ = page_arena.reset(.{ .retain_with_limit = 1 * 1024 * 1024 });
_ = self.browser.state_pool.reset(.{ .retain_with_limit = 4 * 1024 });
// Start netsurf memory arena.
// We need to init this early as JS event handlers may be registered through Runtime.evaluate before the first html doc is loaded
try parser.init(page_arena.allocator());
self.page = @as(Page, undefined);
const page = &self.page.?;
try Page.init(page, page_arena.allocator(), self);

View File

@@ -12,7 +12,6 @@ pub const LookupOpts = struct {
request_time: ?i64 = null,
origin_uri: ?*const Uri = null,
navigation: bool = true,
is_http: bool,
};
pub const Jar = struct {
@@ -33,13 +32,6 @@ pub const Jar = struct {
self.cookies.deinit(self.allocator);
}
pub fn clearRetainingCapacity(self: *Jar) void {
for (self.cookies.items) |c| {
c.deinit();
}
self.cookies.clearRetainingCapacity();
}
pub fn add(
self: *Jar,
cookie: Cookie,
@@ -67,33 +59,87 @@ pub const Jar = struct {
}
}
pub fn removeExpired(self: *Jar, request_time: ?i64) void {
if (self.cookies.items.len == 0) return;
const time = request_time orelse std.time.timestamp();
var i: usize = self.cookies.items.len - 1;
while (i > 0) {
defer i -= 1;
const cookie = &self.cookies.items[i];
if (isCookieExpired(cookie, time)) {
self.cookies.swapRemove(i).deinit();
}
}
}
pub fn forRequest(self: *Jar, target_uri: *const Uri, writer: anytype, opts: LookupOpts) !void {
const target = PreparedUri{
.host = (target_uri.host orelse return error.InvalidURI).percent_encoded,
.path = target_uri.path.percent_encoded,
.secure = std.mem.eql(u8, target_uri.scheme, "https"),
};
const same_site = try areSameSite(opts.origin_uri, target.host);
const target_path = target_uri.path.percent_encoded;
const target_host = (target_uri.host orelse return error.InvalidURI).percent_encoded;
removeExpired(self, opts.request_time);
const same_site = try areSameSite(opts.origin_uri, target_host);
const is_secure = std.mem.eql(u8, target_uri.scheme, "https");
var i: usize = 0;
var cookies = self.cookies.items;
const navigation = opts.navigation;
const request_time = opts.request_time orelse std.time.timestamp();
var first = true;
for (self.cookies.items) |*cookie| {
if (!cookie.appliesTo(&target, same_site, opts.navigation, opts.is_http)) continue;
while (i < cookies.len) {
const cookie = &cookies[i];
if (isCookieExpired(cookie, request_time)) {
cookie.deinit();
_ = self.cookies.swapRemove(i);
// don't increment i !
continue;
}
i += 1;
if (is_secure == false and cookie.secure) {
// secure cookie can only be sent over HTTPs
continue;
}
if (same_site == false) {
// If we aren't on the "same site" (matching 2nd level domain
// taking into account public suffix list), then the cookie
// can only be sent if cookie.same_site == .none, or if
// we're navigating to (as opposed to, say, loading an image)
// and cookie.same_site == .lax
switch (cookie.same_site) {
.strict => continue,
.lax => if (navigation == false) continue,
.none => {},
}
}
{
const domain = cookie.domain;
if (domain[0] == '.') {
// When a Set-Cookie header has a Domain attribute
// Then we will _always_ prefix it with a dot, extending its
// availability to all subdomains (yes, setting the Domain
// attributes EXPANDS the domains which the cookie will be
// sent to, to always include all subdomains).
if (std.mem.eql(u8, target_host, domain[1..]) == false and std.mem.endsWith(u8, target_host, domain) == false) {
continue;
}
} else if (std.mem.eql(u8, target_host, domain) == false) {
// When the Domain attribute isn't specific, then the cookie
// is only sent on an exact match.
continue;
}
}
{
const path = cookie.path;
if (path[path.len - 1] == '/') {
// If our cookie has a trailing slash, we can only match is
// the target path is a perfix. I.e., if our path is
// /doc/ we can only match /doc/*
if (std.mem.startsWith(u8, target_path, path) == false) {
continue;
}
} else {
// Our cookie path is something like /hello
if (std.mem.startsWith(u8, target_path, path) == false) {
// The target path has to either be /hello (it isn't)
continue;
} else if (target_path.len < path.len or (target_path.len > path.len and target_path[path.len] != '/')) {
// Or it has to be something like /hello/* (it isn't)
// it isn't!
continue;
}
}
}
// we have a match!
if (first) {
first = false;
@@ -127,9 +173,47 @@ pub const Jar = struct {
}
};
pub const CookieList = struct {
_cookies: std.ArrayListUnmanaged(*const Cookie) = .{},
pub fn deinit(self: *CookieList, allocator: Allocator) void {
self._cookies.deinit(allocator);
}
pub fn cookies(self: *const CookieList) []*const Cookie {
return self._cookies.items;
}
pub fn len(self: *const CookieList) usize {
return self._cookies.items.len;
}
pub fn write(self: *const CookieList, writer: anytype) !void {
const all = self._cookies.items;
if (all.len == 0) {
return;
}
try writeCookie(all[0], writer);
for (all[1..]) |cookie| {
try writer.writeAll("; ");
try writeCookie(cookie, writer);
}
}
fn writeCookie(cookie: *const Cookie, writer: anytype) !void {
if (cookie.name.len > 0) {
try writer.writeAll(cookie.name);
try writer.writeByte('=');
}
if (cookie.value.len > 0) {
try writer.writeAll(cookie.value);
}
}
};
fn isCookieExpired(cookie: *const Cookie, now: i64) bool {
const ce = cookie.expires orelse return false;
return ce <= @as(f64, @floatFromInt(now));
return ce <= now;
}
fn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool {
@@ -172,12 +256,12 @@ pub const Cookie = struct {
arena: ArenaAllocator,
name: []const u8,
value: []const u8,
domain: []const u8,
path: []const u8,
expires: ?f64,
secure: bool = false,
http_only: bool = false,
same_site: SameSite = .none,
domain: []const u8,
expires: ?i64,
secure: bool,
http_only: bool,
same_site: SameSite,
const SameSite = enum {
strict,
@@ -208,6 +292,9 @@ pub const Cookie = struct {
// this check is necessary, `std.mem.minMax` asserts len > 0
return error.Empty;
}
const host = (uri.host orelse return error.InvalidURI).percent_encoded;
{
const min, const max = std.mem.minMax(u8, str);
if (min < 32 or max > 126) {
@@ -226,7 +313,7 @@ pub const Cookie = struct {
var secure: ?bool = null;
var max_age: ?i64 = null;
var http_only: ?bool = null;
var expires: ?[]const u8 = null;
var expires: ?DateTime = null;
var same_site: ?Cookie.SameSite = null;
var it = std.mem.splitScalar(u8, rest, ';');
@@ -252,13 +339,37 @@ pub const Cookie = struct {
samesite,
}, std.ascii.lowerString(&scrap, key_string)) orelse continue;
const value = if (sep == attribute.len) "" else trim(attribute[sep + 1 ..]);
var value = if (sep == attribute.len) "" else trim(attribute[sep + 1 ..]);
switch (key) {
.path => path = value,
.domain => domain = value,
.path => {
// path attribute value either begins with a '/' or we
// ignore it and use the "default-path" algorithm
if (value.len > 0 and value[0] == '/') {
path = value;
}
},
.domain => {
if (value.len == 0) {
continue;
}
if (value[0] == '.') {
// leading dot is ignored
value = value[1..];
}
if (std.mem.indexOfScalarPos(u8, value, 0, '.') == null and std.ascii.eqlIgnoreCase("localhost", value) == false) {
// can't set a cookie for a TLD
return error.InvalidDomain;
}
if (std.mem.endsWith(u8, host, value) == false) {
return error.InvalidDomain;
}
domain = value;
},
.secure => secure = true,
.@"max-age" => max_age = std.fmt.parseInt(i64, value, 10) catch continue,
.expires => expires = value,
.expires => expires = DateTime.parse(value, .rfc822) catch continue,
.httponly => http_only = true,
.samesite => {
same_site = std.meta.stringToEnum(Cookie.SameSite, std.ascii.lowerString(&scrap, value)) orelse continue;
@@ -275,28 +386,27 @@ pub const Cookie = struct {
const aa = arena.allocator();
const owned_name = try aa.dupe(u8, cookie_name);
const owned_value = try aa.dupe(u8, cookie_value);
const owned_path = try parsePath(aa, uri, path);
const owned_domain = try parseDomain(aa, uri, domain);
const owned_path = if (path) |p|
try aa.dupe(u8, p)
else
try defaultPath(aa, uri.path.percent_encoded);
var normalized_expires: ?f64 = null;
const owned_domain = if (domain) |d| blk: {
const s = try aa.alloc(u8, d.len + 1);
s[0] = '.';
@memcpy(s[1..], d);
break :blk s;
} else blk: {
break :blk try aa.dupe(u8, host);
};
var normalized_expires: ?i64 = null;
if (max_age) |ma| {
normalized_expires = @floatFromInt(std.time.timestamp() + ma);
normalized_expires = std.time.timestamp() + ma;
} else {
// max age takes priority over expires
if (expires) |expires_| {
var exp_dt = DateTime.parse(expires_, .rfc822) catch null;
if (exp_dt == null) {
if ((expires_.len > 11 and expires_[7] == '-' and expires_[11] == '-')) {
// Replace dashes and try again
const output = try aa.dupe(u8, expires_);
output[7] = ' ';
output[11] = ' ';
exp_dt = DateTime.parse(output, .rfc822) catch null;
}
}
if (exp_dt) |dt| {
normalized_expires = @floatFromInt(dt.unix(.seconds));
} else std.debug.print("Invalid cookie expires value: {s}\n", .{expires_});
if (expires) |e| {
normalized_expires = e.sub(DateTime.now(), .seconds);
}
}
@@ -313,100 +423,6 @@ pub const Cookie = struct {
};
}
pub fn parsePath(arena: Allocator, uri: ?*const std.Uri, explicit_path: ?[]const u8) ![]const u8 {
// path attribute value either begins with a '/' or we
// ignore it and use the "default-path" algorithm
if (explicit_path) |path| {
if (path.len > 0 and path[0] == '/') {
return try arena.dupe(u8, path);
}
}
// default-path
const url_path = (uri orelse return "/").path;
const either = url_path.percent_encoded;
if (either.len == 0 or (either.len == 1 and either[0] == '/')) {
return "/";
}
var owned_path: []const u8 = try percentEncode(arena, url_path, isPathChar);
const last = std.mem.lastIndexOfScalar(u8, owned_path[1..], '/') orelse {
return "/";
};
return try arena.dupe(u8, owned_path[0 .. last + 1]);
}
pub fn parseDomain(arena: Allocator, uri: ?*const std.Uri, explicit_domain: ?[]const u8) ![]const u8 {
var encoded_host: ?[]const u8 = null;
if (uri) |uri_| {
const uri_host = uri_.host orelse return error.InvalidURI;
const host = try percentEncode(arena, uri_host, isHostChar);
_ = toLower(host);
encoded_host = host;
}
if (explicit_domain) |domain| {
if (domain.len > 0) {
const no_leading_dot = if (domain[0] == '.') domain[1..] else domain;
var list = std.ArrayList(u8).init(arena);
try list.ensureTotalCapacity(no_leading_dot.len + 1); // Expect no precents needed
list.appendAssumeCapacity('.');
try std.Uri.Component.percentEncode(list.writer(), no_leading_dot, isHostChar);
var owned_domain: []u8 = list.items; // @memory retains memory used before growing
_ = toLower(owned_domain);
if (std.mem.indexOfScalarPos(u8, owned_domain, 1, '.') == null and std.mem.eql(u8, "localhost", owned_domain[1..]) == false) {
// can't set a cookie for a TLD
return error.InvalidDomain;
}
if (encoded_host) |host| {
if (std.mem.endsWith(u8, host, owned_domain[1..]) == false) {
return error.InvalidDomain;
}
}
return owned_domain;
}
}
return encoded_host orelse return error.InvalidDomain; // default-domain
}
pub fn percentEncode(arena: Allocator, component: std.Uri.Component, comptime isValidChar: fn (u8) bool) ![]u8 {
switch (component) {
.raw => |str| {
var list = std.ArrayList(u8).init(arena);
try list.ensureTotalCapacity(str.len); // Expect no precents needed
try std.Uri.Component.percentEncode(list.writer(), str, isValidChar);
return list.items; // @memory retains memory used before growing
},
.percent_encoded => |str| {
return try arena.dupe(u8, str);
},
}
}
pub fn isHostChar(c: u8) bool {
return switch (c) {
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
':' => true,
'[', ']' => true,
else => false,
};
}
pub fn isPathChar(c: u8) bool {
return switch (c) {
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
'/', ':', '@' => true,
else => false,
};
}
fn parseNameValue(str: []const u8) !struct { []const u8, []const u8, []const u8 } {
const key_value_end = std.mem.indexOfScalarPos(u8, str, 0, ';') orelse str.len;
const rest = if (key_value_end == str.len) "" else str[key_value_end + 1 ..];
@@ -423,77 +439,17 @@ pub const Cookie = struct {
const value = trim(str[sep + 1 .. key_value_end]);
return .{ name, value, rest };
}
};
pub fn appliesTo(self: *const Cookie, url: *const PreparedUri, same_site: bool, navigation: bool, is_http: bool) bool {
if (self.http_only and is_http == false) {
// http only cookies can be accessed from Javascript
return false;
}
if (url.secure == false and self.secure) {
// secure cookie can only be sent over HTTPs
return false;
}
if (same_site == false) {
// If we aren't on the "same site" (matching 2nd level domain
// taking into account public suffix list), then the cookie
// can only be sent if cookie.same_site == .none, or if
// we're navigating to (as opposed to, say, loading an image)
// and cookie.same_site == .lax
switch (self.same_site) {
.strict => return false,
.lax => if (navigation == false) return false,
.none => {},
}
}
{
if (self.domain[0] == '.') {
// When a Set-Cookie header has a Domain attribute
// Then we will _always_ prefix it with a dot, extending its
// availability to all subdomains (yes, setting the Domain
// attributes EXPANDS the domains which the cookie will be
// sent to, to always include all subdomains).
if (std.mem.eql(u8, url.host, self.domain[1..]) == false and std.mem.endsWith(u8, url.host, self.domain) == false) {
return false;
}
} else if (std.mem.eql(u8, url.host, self.domain) == false) {
// When the Domain attribute isn't specific, then the cookie
// is only sent on an exact match.
return false;
}
}
{
if (self.path[self.path.len - 1] == '/') {
// If our cookie has a trailing slash, we can only match is
// the target path is a perfix. I.e., if our path is
// /doc/ we can only match /doc/*
if (std.mem.startsWith(u8, url.path, self.path) == false) {
return false;
}
} else {
// Our cookie path is something like /hello
if (std.mem.startsWith(u8, url.path, self.path) == false) {
// The target path has to either be /hello (it isn't)
return false;
} else if (url.path.len < self.path.len or (url.path.len > self.path.len and url.path[self.path.len] != '/')) {
// Or it has to be something like /hello/* (it isn't)
// it isn't!
return false;
}
}
}
return true;
fn defaultPath(allocator: Allocator, document_path: []const u8) ![]const u8 {
if (document_path.len == 0 or (document_path.len == 1 and document_path[0] == '/')) {
return "/";
}
};
pub const PreparedUri = struct {
host: []const u8, // Percent encoded, lower case
path: []const u8, // Percent encoded
secure: bool, // True if scheme is https
};
const last = std.mem.lastIndexOfScalar(u8, document_path[1..], '/') orelse {
return "/";
};
return try allocator.dupe(u8, document_path[0 .. last + 1]);
}
fn trim(str: []const u8) []const u8 {
return std.mem.trim(u8, str, &std.ascii.whitespace);
@@ -507,13 +463,6 @@ fn trimRight(str: []const u8) []const u8 {
return std.mem.trimLeft(u8, str, &std.ascii.whitespace);
}
pub fn toLower(str: []u8) []u8 {
for (str, 0..) |c, i| {
str[i] = std.ascii.toLower(c);
}
return str;
}
const testing = @import("../../testing.zig");
test "cookie: findSecondLevelDomain" {
const cases = [_]struct { []const u8, []const u8 }{
@@ -599,7 +548,7 @@ test "Jar: forRequest" {
{
// test with no cookies
try expectCookies("", &jar, test_uri, .{ .is_http = true });
try expectCookies("", &jar, test_uri, .{});
}
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global1=1"), now);
@@ -613,114 +562,97 @@ test "Jar: forRequest" {
try jar.add(try Cookie.parse(testing.allocator, &test_uri_2, "domain1=9;domain=test.lightpanda.io"), now);
// nothing fancy here
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .is_http = true });
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .navigation = false, .is_http = true });
try expectCookies("global1=1; global2=2", &jar, test_uri, .{});
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .navigation = false });
// We have a cookie where Domain=lightpanda.io
// This should _not_ match xyxlightpanda.io
try expectCookies("", &jar, try std.Uri.parse("http://anothersitelightpanda.io/"), .{
.origin_uri = &test_uri,
.is_http = true,
});
// matching path without trailing /
try expectCookies("global1=1; global2=2; path1=3", &jar, try std.Uri.parse("http://lightpanda.io/about"), .{
.origin_uri = &test_uri,
.is_http = true,
});
// incomplete prefix path
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/abou"), .{
.origin_uri = &test_uri,
.is_http = true,
});
// path doesn't match
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/aboutus"), .{
.origin_uri = &test_uri,
.is_http = true,
});
// path doesn't match cookie directory
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/docs"), .{
.origin_uri = &test_uri,
.is_http = true,
});
// exact directory match
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/"), .{
.origin_uri = &test_uri,
.is_http = true,
});
// sub directory match
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/more"), .{
.origin_uri = &test_uri,
.is_http = true,
});
// secure
try expectCookies("global1=1; global2=2; secure=5", &jar, try std.Uri.parse("https://lightpanda.io/"), .{
.origin_uri = &test_uri,
.is_http = true,
});
// navigational cross domain, secure
try expectCookies("global1=1; global2=2; secure=5; sitenone=6; sitelax=7", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")),
.is_http = true,
});
// navigational cross domain, insecure
try expectCookies("global1=1; global2=2; sitelax=7", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")),
.is_http = true,
});
// non-navigational cross domain, insecure
try expectCookies("", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")),
.navigation = false,
.is_http = true,
});
// non-navigational cross domain, secure
try expectCookies("sitenone=6", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")),
.navigation = false,
.is_http = true,
});
// non-navigational same origin
try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://lightpanda.io/")),
.navigation = false,
.is_http = true,
});
// exact domain match + suffix
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://test.lightpanda.io/"), .{
.origin_uri = &test_uri,
.is_http = true,
});
// domain suffix match + suffix
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://1.test.lightpanda.io/"), .{
.origin_uri = &test_uri,
.is_http = true,
});
// non-matching domain
try expectCookies("global2=2", &jar, try std.Uri.parse("http://other.lightpanda.io/"), .{
.origin_uri = &test_uri,
.is_http = true,
});
const l = jar.cookies.items.len;
try expectCookies("global1=1", &jar, test_uri, .{
.request_time = now + 100,
.origin_uri = &test_uri,
.is_http = true,
});
try testing.expectEqual(l - 1, jar.cookies.items.len);
@@ -728,6 +660,40 @@ test "Jar: forRequest" {
// the 'global2' cookie
}
test "CookieList: write" {
var arr: std.ArrayListUnmanaged(u8) = .{};
defer arr.deinit(testing.allocator);
var cookie_list = CookieList{};
defer cookie_list.deinit(testing.allocator);
const c1 = try Cookie.parse(testing.allocator, &test_uri, "cookie_name=cookie_value");
defer c1.deinit();
{
try cookie_list._cookies.append(testing.allocator, &c1);
try cookie_list.write(arr.writer(testing.allocator));
try testing.expectEqual("cookie_name=cookie_value", arr.items);
}
const c2 = try Cookie.parse(testing.allocator, &test_uri, "x84");
defer c2.deinit();
{
arr.clearRetainingCapacity();
try cookie_list._cookies.append(testing.allocator, &c2);
try cookie_list.write(arr.writer(testing.allocator));
try testing.expectEqual("cookie_name=cookie_value; x84", arr.items);
}
const c3 = try Cookie.parse(testing.allocator, &test_uri, "nope=");
defer c3.deinit();
{
arr.clearRetainingCapacity();
try cookie_list._cookies.append(testing.allocator, &c3);
try cookie_list.write(arr.writer(testing.allocator));
try testing.expectEqual("cookie_name=cookie_value; x84; nope=", arr.items);
}
}
test "Cookie: parse key=value" {
try expectError(error.Empty, null, "");
try expectError(error.InvalidByteSequence, null, &.{ 'a', 30, '=', 'b' });
@@ -850,8 +816,7 @@ test "Cookie: parse expires" {
try expectAttribute(.{ .expires = null }, null, "b;expires=13.22");
try expectAttribute(.{ .expires = null }, null, "b;expires=33");
try expectAttribute(.{ .expires = 1918798080 }, null, "b;expires=Wed, 21 Oct 2030 07:28:00 GMT");
try expectAttribute(.{ .expires = 1784275395 }, null, "b;expires=Fri, 17-Jul-2026 08:03:15 GMT");
try expectAttribute(.{ .expires = 1918798080 - std.time.timestamp() }, null, "b;expires=Wed, 21 Oct 2030 07:28:00 GMT");
// max-age has priority over expires
try expectAttribute(.{ .expires = std.time.timestamp() + 10 }, null, "b;Max-Age=10; expires=Wed, 21 Oct 2030 07:28:00 GMT");
}
@@ -871,7 +836,7 @@ test "Cookie: parse all" {
.http_only = true,
.secure = true,
.domain = ".lightpanda.io",
.expires = @floatFromInt(std.time.timestamp() + 30),
.expires = std.time.timestamp() + 30,
}, "https://lightpanda.io/cms/users", "user-id=9000; HttpOnly; Max-Age=30; Secure; path=/; Domain=lightpanda.io");
try expectCookie(.{
@@ -882,7 +847,7 @@ test "Cookie: parse all" {
.secure = false,
.domain = ".localhost",
.same_site = .lax,
.expires = @floatFromInt(std.time.timestamp() + 7200),
.expires = std.time.timestamp() + 7200,
}, "http://localhost:8000/login", "app_session=123; Max-Age=7200; path=/; domain=localhost; httponly; samesite=lax");
}
@@ -909,7 +874,7 @@ const ExpectedCookie = struct {
value: []const u8,
path: []const u8,
domain: []const u8,
expires: ?f64 = null,
expires: ?i64 = null,
secure: bool = false,
http_only: bool = false,
same_site: Cookie.SameSite = .lax,
@@ -928,7 +893,7 @@ fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u
try testing.expectEqual(expected.path, cookie.path);
try testing.expectEqual(expected.domain, cookie.domain);
try testing.expectDelta(expected.expires, cookie.expires, 2.0);
try testing.expectDelta(expected.expires, cookie.expires, 2);
}
fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) !void {
@@ -938,10 +903,7 @@ fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8)
inline for (@typeInfo(@TypeOf(expected)).@"struct".fields) |f| {
if (comptime std.mem.eql(u8, f.name, "expires")) {
switch (@typeInfo(@TypeOf(expected.expires))) {
.int, .comptime_int => try testing.expectDelta(@as(f64, @floatFromInt(expected.expires)), cookie.expires, 1.0),
else => try testing.expectDelta(expected.expires, cookie.expires, 1.0),
}
try testing.expectDelta(expected.expires, cookie.expires, 1);
} else {
try testing.expectEqual(@field(expected, f.name), @field(cookie, f.name));
}

View File

@@ -1,91 +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/>.
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;
const Element = @import("../dom/element.zig").Element;
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);
}
};
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 = 'Hello World';
\\ }
\\ }
,
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", "Hello World" },
// 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" },
}, .{});
}

View File

@@ -1,23 +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/>.
const CustomElementRegistry = @import("custom_element_registry.zig").CustomElementRegistry;
pub const Interfaces = .{
CustomElementRegistry,
};

View File

@@ -115,24 +115,17 @@ 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;
// 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;
const collection = try parser.formGetCollection(form);
const len = try parser.htmlCollectionGetLength(collection);
var entries: kv.List = .{};
try entries.ensureTotalCapacity(arena, nodes.len);
try entries.ensureTotalCapacity(arena, len);
var submitter_included = false;
const submitter_name_ = try getSubmitterName(submitter_);
for (nodes) |node| {
for (0..len) |i| {
const node = try parser.htmlCollectionItem(collection, @intCast(i));
const element = parser.nodeToElement(node);
// must have a name
@@ -188,7 +181,10 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
submitter_included = true;
}
},
else => unreachable,
else => {
log.warn(.web_api, "unsupported form element", .{ .tag = @tagName(tag) });
continue;
},
}
}
@@ -301,7 +297,6 @@ 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();
@@ -361,8 +356,6 @@ 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 },
@@ -385,7 +378,6 @@ test "Browser.FormData" {
\\mlt-2=water
\\mlt-2=tea
\\s1=s1-v
\\dyn=dyn-v
},
}, .{});
}

View File

@@ -475,7 +475,6 @@ pub const XMLHttpRequest = struct {
try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(self.arena), .{
.navigation = false,
.origin_uri = &self.origin_url.uri,
.is_http = true,
});
if (arr.items.len > 0) {
@@ -757,7 +756,8 @@ pub const XMLHttpRequest = struct {
}
var fbs = std.io.fixedBufferStream(self.response_bytes.items);
const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch {
const Elements = @import("../html/elements.zig");
const doc = parser.documentHTMLParse(fbs.reader(), ccharset, &Elements.createElement) catch {
self.response_obj = .{ .Failure = {} };
return;
};

View File

@@ -23,6 +23,7 @@ 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;
@@ -181,42 +182,41 @@ pub fn CDPT(comptime TypeProvider: type) type {
switch (domain.len) {
3 => switch (@as(u24, @bitCast(domain[0..3].*))) {
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),
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),
else => {},
},
4 => switch (@as(u32, @bitCast(domain[0..4].*))) {
asUint(u32, "Page") => return @import("domains/page.zig").processMessage(command),
asUint("Page") => return @import("domains/page.zig").processMessage(command),
else => {},
},
5 => switch (@as(u40, @bitCast(domain[0..5].*))) {
asUint(u40, "Fetch") => return @import("domains/fetch.zig").processMessage(command),
asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command),
asUint("Fetch") => return @import("domains/fetch.zig").processMessage(command),
asUint("Input") => return @import("domains/input.zig").processMessage(command),
else => {},
},
6 => switch (@as(u48, @bitCast(domain[0..6].*))) {
asUint(u48, "Target") => return @import("domains/target.zig").processMessage(command),
asUint("Target") => return @import("domains/target.zig").processMessage(command),
else => {},
},
7 => switch (@as(u56, @bitCast(domain[0..7].*))) {
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),
asUint(u56, "Storage") => return @import("domains/storage.zig").processMessage(command),
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),
else => {},
},
8 => switch (@as(u64, @bitCast(domain[0..8].*))) {
asUint(u64, "Security") => return @import("domains/security.zig").processMessage(command),
asUint("Security") => return @import("domains/security.zig").processMessage(command),
else => {},
},
9 => switch (@as(u72, @bitCast(domain[0..9].*))) {
asUint(u72, "Emulation") => return @import("domains/emulation.zig").processMessage(command),
asUint(u72, "Inspector") => return @import("domains/inspector.zig").processMessage(command),
asUint("Emulation") => return @import("domains/emulation.zig").processMessage(command),
asUint("Inspector") => return @import("domains/inspector.zig").processMessage(command),
else => {},
},
11 => switch (@as(u88, @bitCast(domain[0..11].*))) {
asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command),
asUint("Performance") => return @import("domains/performance.zig").processMessage(command),
else => {},
},
else => {},
@@ -320,7 +320,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
inspector: Inspector,
isolated_world: ?IsolatedWorld,
http_proxy_before: ??std.Uri = null,
const Self = @This();
@@ -375,8 +374,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
self.node_registry.deinit();
self.node_search_list.deinit();
self.cdp.browser.notification.unregisterAll(self);
if (self.http_proxy_before) |prev_proxy| self.cdp.browser.http_client.http_proxy = prev_proxy;
}
pub fn reset(self: *Self) void {
@@ -699,10 +696,6 @@ 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();

View File

@@ -17,11 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const Notification = @import("../../notification.zig").Notification;
const log = @import("../../log.zig");
const CdpStorage = @import("storage.zig");
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
@@ -29,11 +28,6 @@ pub fn processMessage(cmd: anytype) !void {
disable,
setCacheDisabled,
setExtraHTTPHeaders,
deleteCookies,
clearBrowserCookies,
setCookie,
setCookies,
getCookies,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
@@ -41,11 +35,6 @@ pub fn processMessage(cmd: anytype) !void {
.disable => return disable(cmd),
.setCacheDisabled => return cmd.sendResult(null, .{}),
.setExtraHTTPHeaders => return setExtraHTTPHeaders(cmd),
.deleteCookies => return deleteCookies(cmd),
.clearBrowserCookies => return clearBrowserCookies(cmd),
.setCookie => return setCookie(cmd),
.setCookies => return setCookies(cmd),
.getCookies => return getCookies(cmd),
}
}
@@ -82,112 +71,6 @@ fn setExtraHTTPHeaders(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
const Cookie = @import("../../browser/storage/storage.zig").Cookie;
// Only matches the cookie on provided parameters
fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, path: ?[]const u8) bool {
if (!std.mem.eql(u8, cookie.name, name)) return false;
if (domain) |domain_| {
const c_no_dot = if (std.mem.startsWith(u8, cookie.domain, ".")) cookie.domain[1..] else cookie.domain;
const d_no_dot = if (std.mem.startsWith(u8, domain_, ".")) domain_[1..] else domain_;
if (!std.mem.eql(u8, c_no_dot, d_no_dot)) return false;
}
if (path) |path_| {
if (!std.mem.eql(u8, cookie.path, path_)) return false;
}
return true;
}
fn deleteCookies(cmd: anytype) !void {
const params = (try cmd.params(struct {
name: []const u8,
url: ?[]const u8 = null,
domain: ?[]const u8 = null,
path: ?[]const u8 = null,
partitionKey: ?CdpStorage.CookiePartitionKey = null,
})) orelse return error.InvalidParams;
if (params.partitionKey != null) return error.NotYetImplementedParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const cookies = &bc.session.cookie_jar.cookies;
const uri = if (params.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
const uri_ptr = if (uri) |u| &u else null;
var index = cookies.items.len;
while (index > 0) {
index -= 1;
const cookie = &cookies.items[index];
const domain = try Cookie.parseDomain(cmd.arena, uri_ptr, params.domain);
const path = try Cookie.parsePath(cmd.arena, uri_ptr, params.path);
// We do not want to use Cookie.appliesTo here. As a Cookie with a shorter path would match.
// Similar to deduplicating with areCookiesEqual, except domain and path are optional.
if (cookieMatches(cookie, params.name, domain, path)) {
cookies.swapRemove(index).deinit();
}
}
return cmd.sendResult(null, .{});
}
fn clearBrowserCookies(cmd: anytype) !void {
if (try cmd.params(struct {}) != null) return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
bc.session.cookie_jar.clearRetainingCapacity();
return cmd.sendResult(null, .{});
}
fn setCookie(cmd: anytype) !void {
const params = (try cmd.params(
CdpStorage.CdpCookie,
)) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
try CdpStorage.setCdpCookie(&bc.session.cookie_jar, params);
try cmd.sendResult(.{ .success = true }, .{});
}
fn setCookies(cmd: anytype) !void {
const params = (try cmd.params(struct {
cookies: []const CdpStorage.CdpCookie,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
for (params.cookies) |param| {
try CdpStorage.setCdpCookie(&bc.session.cookie_jar, param);
}
try cmd.sendResult(null, .{});
}
const GetCookiesParam = struct { urls: ?[]const []const u8 = null };
fn getCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{};
// If not specified, use the URLs of the page and all of its subframes. TODO subframes
const page_url = if (bc.session.page) |*page| page.url.raw else null; // @speed: avoid repasing the URL
const param_urls = params.urls orelse &[_][]const u8{page_url orelse return error.InvalidParams};
var urls = try std.ArrayListUnmanaged(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len);
for (param_urls) |url| {
const uri = std.Uri.parse(url) catch return error.InvalidParams;
urls.appendAssumeCapacity(.{
.host = try Cookie.parseDomain(cmd.arena, &uri, null),
.path = try Cookie.parsePath(cmd.arena, &uri, null),
.secure = std.mem.eql(u8, uri.scheme, "https"),
});
}
var jar = &bc.session.cookie_jar;
jar.removeExpired(null);
const writer = CdpStorage.CookieWriter{ .cookies = jar.cookies.items, .urls = urls.items };
try cmd.sendResult(.{ .cookies = writer }, .{});
}
// Upsert a header into the headers array.
// returns true if the header was added, false if it was updated
fn putAssumeCapacity(headers: *std.ArrayListUnmanaged(std.http.Header), extra: std.http.Header) bool {
@@ -352,77 +235,3 @@ test "cdp.network setExtraHTTPHeaders" {
try ctx.processMessage(.{ .id = 5, .method = "Target.attachToTarget", .params = .{ .targetId = bc.target_id.? } });
try testing.expectEqual(bc.cdp.extra_headers.items.len, 0);
}
test "cdp.Network: cookies" {
const ResCookie = CdpStorage.ResCookie;
const CdpCookie = CdpStorage.CdpCookie;
var ctx = testing.context();
defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-S" });
// Initially empty
try ctx.processMessage(.{
.id = 3,
.method = "Network.getCookies",
.params = .{ .urls = &[_][]const u8{"https://example.com/pancakes"} },
});
try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 3 });
// Has cookies after setting them
try ctx.processMessage(.{
.id = 4,
.method = "Network.setCookie",
.params = CdpCookie{ .name = "test3", .value = "valuenot3", .url = "https://car.example.com/defnotpancakes" },
});
try ctx.expectSentResult(null, .{ .id = 4 });
try ctx.processMessage(.{
.id = 5,
.method = "Network.setCookies",
.params = .{
.cookies = &[_]CdpCookie{
.{ .name = "test3", .value = "value3", .url = "https://car.example.com/pan/cakes" },
.{ .name = "test4", .value = "value4", .domain = "example.com", .path = "/mango" },
},
},
});
try ctx.expectSentResult(null, .{ .id = 5 });
try ctx.processMessage(.{
.id = 6,
.method = "Network.getCookies",
.params = .{ .urls = &[_][]const u8{"https://car.example.com/pan/cakes"} },
});
try ctx.expectSentResult(.{
.cookies = &[_]ResCookie{
.{ .name = "test3", .value = "value3", .domain = "car.example.com", .path = "/", .secure = true }, // No Pancakes!
},
}, .{ .id = 6 });
// deleteCookies
try ctx.processMessage(.{
.id = 7,
.method = "Network.deleteCookies",
.params = .{ .name = "test3", .domain = "car.example.com" },
});
try ctx.expectSentResult(null, .{ .id = 7 });
try ctx.processMessage(.{
.id = 8,
.method = "Storage.getCookies",
.params = .{ .browserContextId = "BID-S" },
});
// Just the untouched test4 should be in the result
try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{.{ .name = "test4", .value = "value4", .domain = ".example.com", .path = "/mango" }} }, .{ .id = 8 });
// Empty after clearBrowserCookies
try ctx.processMessage(.{
.id = 9,
.method = "Network.clearBrowserCookies",
});
try ctx.expectSentResult(null, .{ .id = 9 });
try ctx.processMessage(.{
.id = 10,
.method = "Storage.getCookies",
.params = .{ .browserContextId = "BID-S" },
});
try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 10 });
}

View File

@@ -1,301 +0,0 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const log = @import("../../log.zig");
const Cookie = @import("../../browser/storage/storage.zig").Cookie;
const CookieJar = @import("../../browser/storage/storage.zig").CookieJar;
pub const PreparedUri = @import("../../browser/storage/cookie.zig").PreparedUri;
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
clearCookies,
setCookies,
getCookies,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
.clearCookies => return clearCookies(cmd),
.getCookies => return getCookies(cmd),
.setCookies => return setCookies(cmd),
}
}
const BrowserContextParam = struct { browserContextId: ?[]const u8 = null };
fn clearCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{};
if (params.browserContextId) |browser_context_id| {
if (std.mem.eql(u8, browser_context_id, bc.id) == false) {
return error.UnknownBrowserContextId;
}
}
bc.session.cookie_jar.clearRetainingCapacity();
return cmd.sendResult(null, .{});
}
fn getCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{};
if (params.browserContextId) |browser_context_id| {
if (std.mem.eql(u8, browser_context_id, bc.id) == false) {
return error.UnknownBrowserContextId;
}
}
bc.session.cookie_jar.removeExpired(null);
const writer = CookieWriter{ .cookies = bc.session.cookie_jar.cookies.items };
try cmd.sendResult(.{ .cookies = writer }, .{});
}
fn setCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
cookies: []const CdpCookie,
browserContextId: ?[]const u8 = null,
})) orelse return error.InvalidParams;
if (params.browserContextId) |browser_context_id| {
if (std.mem.eql(u8, browser_context_id, bc.id) == false) {
return error.UnknownBrowserContextId;
}
}
for (params.cookies) |param| {
try setCdpCookie(&bc.session.cookie_jar, param);
}
try cmd.sendResult(null, .{});
}
pub const SameSite = enum {
Strict,
Lax,
None,
};
pub const CookiePriority = enum {
Low,
Medium,
High,
};
pub const CookieSourceScheme = enum {
Unset,
NonSecure,
Secure,
};
pub const CookiePartitionKey = struct {
topLevelSite: []const u8,
hasCrossSiteAncestor: bool,
};
pub const CdpCookie = struct {
name: []const u8,
value: []const u8,
url: ?[]const u8 = null,
domain: ?[]const u8 = null,
path: ?[]const u8 = null,
secure: ?bool = null, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
httpOnly: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
sameSite: SameSite = .None, // default: https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies
expires: ?f64 = null, // -1? says google
priority: CookiePriority = .Medium, // default: https://datatracker.ietf.org/doc/html/draft-west-cookie-priority-00
sameParty: ?bool = null,
sourceScheme: ?CookieSourceScheme = null,
// sourcePort: Temporary ability and it will be removed from CDP
partitionKey: ?CookiePartitionKey = null,
};
pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) {
return error.NotYetImplementedParams;
}
var arena = std.heap.ArenaAllocator.init(cookie_jar.allocator);
errdefer arena.deinit();
const a = arena.allocator();
// NOTE: The param.url can affect the default domain, (NOT path), secure, source port, and source scheme.
const uri = if (param.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
const uri_ptr = if (uri) |*u| u else null;
const domain = try Cookie.parseDomain(a, uri_ptr, param.domain);
const path = if (param.path == null) "/" else try Cookie.parsePath(a, null, param.path);
const secure = if (param.secure) |s| s else if (uri) |uri_| std.mem.eql(u8, uri_.scheme, "https") else false;
const cookie = Cookie{
.arena = arena,
.name = try a.dupe(u8, param.name),
.value = try a.dupe(u8, param.value),
.path = path,
.domain = domain,
.expires = param.expires,
.secure = secure,
.http_only = param.httpOnly,
.same_site = switch (param.sameSite) {
.Strict => .strict,
.Lax => .lax,
.None => .none,
},
};
try cookie_jar.add(cookie, std.time.timestamp());
}
pub const CookieWriter = struct {
cookies: []const Cookie,
urls: ?[]const PreparedUri = null,
pub fn jsonStringify(self: *const CookieWriter, w: anytype) !void {
self.writeCookies(w) catch |err| {
// The only error our jsonStringify method can return is @TypeOf(w).Error.
log.err(.cdp, "json stringify", .{ .err = err });
return error.OutOfMemory;
};
}
fn writeCookies(self: CookieWriter, w: anytype) !void {
try w.beginArray();
if (self.urls) |urls| {
for (self.cookies) |*cookie| {
for (urls) |*url| {
if (cookie.appliesTo(url, true, true, true)) { // TBD same_site, should we compare to the pages url?
try writeCookie(cookie, w);
break;
}
}
}
} else {
for (self.cookies) |*cookie| {
try writeCookie(cookie, w);
}
}
try w.endArray();
}
};
pub fn writeCookie(cookie: *const Cookie, w: anytype) !void {
try w.beginObject();
{
try w.objectField("name");
try w.write(cookie.name);
try w.objectField("value");
try w.write(cookie.value);
try w.objectField("domain");
try w.write(cookie.domain); // Should we hide a leading dot?
try w.objectField("path");
try w.write(cookie.path);
try w.objectField("expires");
try w.write(cookie.expires orelse -1);
// TODO size
try w.objectField("httpOnly");
try w.write(cookie.http_only);
try w.objectField("secure");
try w.write(cookie.secure);
try w.objectField("session");
try w.write(cookie.expires == null);
try w.objectField("sameSite");
switch (cookie.same_site) {
.none => try w.write("None"),
.lax => try w.write("Lax"),
.strict => try w.write("Strict"),
}
// TODO experimentals
}
try w.endObject();
}
const testing = @import("../testing.zig");
test "cdp.Storage: cookies" {
var ctx = testing.context();
defer ctx.deinit();
_ = try ctx.loadBrowserContext(.{ .id = "BID-S" });
// Initially empty
try ctx.processMessage(.{
.id = 3,
.method = "Storage.getCookies",
.params = .{ .browserContextId = "BID-S" },
});
try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 3 });
// Has cookies after setting them
try ctx.processMessage(.{
.id = 4,
.method = "Storage.setCookies",
.params = .{
.cookies = &[_]CdpCookie{
.{ .name = "test", .value = "value", .domain = "example.com", .path = "/mango" },
.{ .name = "test2", .value = "value2", .url = "https://car.example.com/pancakes" },
},
.browserContextId = "BID-S",
},
});
try ctx.expectSentResult(null, .{ .id = 4 });
try ctx.processMessage(.{
.id = 5,
.method = "Storage.getCookies",
.params = .{ .browserContextId = "BID-S" },
});
try ctx.expectSentResult(.{
.cookies = &[_]ResCookie{
.{ .name = "test", .value = "value", .domain = ".example.com", .path = "/mango" },
.{ .name = "test2", .value = "value2", .domain = "car.example.com", .path = "/", .secure = true }, // No Pancakes!
},
}, .{ .id = 5 });
// Empty after clearing cookies
try ctx.processMessage(.{
.id = 6,
.method = "Storage.clearCookies",
.params = .{ .browserContextId = "BID-S" },
});
try ctx.expectSentResult(null, .{ .id = 6 });
try ctx.processMessage(.{
.id = 7,
.method = "Storage.getCookies",
.params = .{ .browserContextId = "BID-S" },
});
try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 7 });
}
pub const ResCookie = struct {
name: []const u8,
value: []const u8,
domain: []const u8,
path: []const u8 = "/",
expires: f64 = -1,
httpOnly: bool = false,
secure: bool = false,
sameSite: []const u8 = "None",
};

View File

@@ -66,30 +66,11 @@ fn getBrowserContexts(cmd: anytype) !void {
}
fn createBrowserContext(cmd: anytype) !void {
const params = try cmd.params(struct {
disposeOnDetach: bool = false,
proxyServer: ?[]const u8 = null,
proxyBypassList: ?[]const u8 = null,
originsWithUniversalNetworkAccess: ?[]const []const u8 = null,
});
if (params) |p| {
if (p.disposeOnDetach or p.proxyBypassList != null or p.originsWithUniversalNetworkAccess != null) std.debug.print("Target.createBrowserContext: Not implemented param set\n", .{});
}
const bc = cmd.createBrowserContext() catch |err| switch (err) {
error.AlreadyExists => return cmd.sendError(-32000, "Cannot have more than one browser context at a time"),
else => return err,
};
if (params) |p| {
if (p.proxyServer) |proxy| {
// For now the http client is not in the browser context so we assume there is just 1.
bc.http_proxy_before = cmd.cdp.browser.http_client.http_proxy;
const proxy_cp = try cmd.cdp.browser.http_client.allocator.dupe(u8, proxy);
cmd.cdp.browser.http_client.http_proxy = try std.Uri.parse(proxy_cp);
}
}
return cmd.sendResult(.{
.browserContextId = bc.id,
}, .{});

View File

@@ -716,7 +716,7 @@ pub const Request = struct {
}
fn newReader(self: *Request) Reader {
return Reader.init(self._state);
return Reader.init(self._state, &self._keepalive);
}
// Does additional setup of the request for the firsts (i.e. non-redirect) call.
@@ -879,14 +879,12 @@ pub const Request = struct {
});
}
fn requestCompleted(self: *Request, response: ResponseHeader, can_keepalive: bool) void {
fn requestCompleted(self: *Request, response: ResponseHeader) void {
const notification = self.notification orelse return;
if (self._notified_complete) {
return;
}
self._notified_complete = true;
self._keepalive = can_keepalive;
notification.dispatch(.http_request_complete, &.{
.id = self.id,
.url = self.request_uri,
@@ -1113,14 +1111,14 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
.handler_error => {
// handler should never have been called if we're redirecting
std.debug.assert(self.redirect == null);
self.request.requestCompleted(self.reader.response, self.reader.keepalive);
self.request.requestCompleted(self.reader.response);
self.deinit();
return;
},
.done => {
const redirect = self.redirect orelse {
var handler = self.handler;
self.request.requestCompleted(self.reader.response, self.reader.keepalive);
self.request.requestCompleted(self.reader.response);
self.deinit();
// Emit the done chunk. We expect the caller to do
@@ -1562,6 +1560,8 @@ const SyncHandler = struct {
var decompressor = std.compress.gzip.decompressor(compress_reader.reader());
try decompressor.decompress(body.writer(request.arena));
self.request.requestCompleted(reader.response);
return .{
.header = reader.response,
._done = true,
@@ -1781,8 +1781,8 @@ const SyncHandler = struct {
// Used for reading the response (both the header and the body)
const Reader = struct {
// Wether, from the reader's point of view, this connection could be kept-alive
keepalive: bool,
// ref request.keepalive
keepalive: *bool,
// always references state.header_buf
header_buf: []u8,
@@ -1802,13 +1802,13 @@ const Reader = struct {
// Whether or not the current header has to be skipped [because it's too long].
skip_current_header: bool,
fn init(state: *State) Reader {
fn init(state: *State, keepalive: *bool) Reader {
return .{
.pos = 0,
.response = .{},
.body_reader = null,
.header_done = false,
.keepalive = false,
.keepalive = keepalive,
.skip_current_header = false,
.header_buf = state.header_buf,
.arena = state.arena.allocator(),
@@ -1835,6 +1835,7 @@ const Reader = struct {
// us to emit whatever data we have, but it isn't safe to keep
// the connection alive.
std.debug.assert(result.done == true);
self.keepalive.* = false;
}
return result;
}
@@ -1929,7 +1930,7 @@ const Reader = struct {
// We think we're done reading the body, but we still have data
// We'll return what we have as-is, but close the connection
// because we don't know what state it's in.
self.keepalive = false;
self.keepalive.* = false;
} else {
result.unprocessed = unprocessed;
}
@@ -1944,7 +1945,7 @@ const Reader = struct {
if (response.get("connection")) |connection| {
if (std.ascii.eqlIgnoreCase(connection, "close")) {
self.keepalive = false;
self.keepalive.* = false;
}
}
@@ -2004,7 +2005,7 @@ const Reader = struct {
}
const protocol = data[0..9];
if (std.mem.eql(u8, protocol, "HTTP/1.1 ")) {
self.keepalive = true;
self.keepalive.* = true;
} else if (std.mem.eql(u8, protocol, "HTTP/1.0 ") == false) {
return error.InvalidStatusLine;
}
@@ -2386,7 +2387,7 @@ pub const Response = struct {
return data;
}
if (self._done) {
self._request.requestCompleted(self.header, self._reader.keepalive);
self._request.requestCompleted(self.header);
return null;
}
@@ -3169,7 +3170,7 @@ test "HttpClient: async tls no body" {
}
}
test "HttpClient: async tls with body" {
test "HttpClient: async tls with body x" {
defer testing.reset();
for (0..5) |_| {
var client = try testClient();
@@ -3395,7 +3396,8 @@ const CaptureHandler = struct {
fn testReader(state: *State, res: *TestResponse, data: []const u8) !void {
var status: u16 = 0;
var r = Reader.init(state);
var keepalive = false;
var r = Reader.init(state, &keepalive);
// dupe it so that we have a mutable copy
const owned = try testing.allocator.dupe(u8, data);

View File

@@ -146,16 +146,6 @@ 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()});
@@ -174,20 +164,15 @@ fn logLogFmtPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8
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(.pretty, @field(data, f.name), writer);
try writer.writeByte('\n');
try writeValue(.logfmt, @field(data, f.name), writer);
}
try writer.writeByte('\n');
}
fn logPrettyPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: anytype) !void {
fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
if (scope == .console and level == .fatal and comptime std.mem.eql(u8, msg, "lightpanda")) {
try writer.writeAll("\x1b[0;104mWARN ");
} else {
@@ -216,6 +201,14 @@ fn logPrettyPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8
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 {

View File

@@ -103,7 +103,7 @@ fn run(alloc: Allocator) !void {
};
},
.fetch => |opts| {
log.debug(.app, "startup", .{ .mode = "fetch", .dump = opts.dump, .markdown = opts.markdown, .url = opts.url });
log.debug(.app, "startup", .{ .mode = "fetch", .dump = opts.dump, .url = opts.url });
const url = try @import("url.zig").URL.parse(opts.url, null);
// browser
@@ -128,13 +128,6 @@ fn run(alloc: Allocator) !void {
try page.wait();
// markdown
if (opts.markdown) {
try page.markdown(std.io.getStdOut());
// do not dump HTML if both options are provided
return;
}
// dump
if (opts.dump) {
try page.dump(std.io.getStdOut());
@@ -200,7 +193,6 @@ const Command = struct {
const Fetch = struct {
url: []const u8,
dump: bool = false,
markdown: bool = false,
common: Common,
};
@@ -249,9 +241,6 @@ const Command = struct {
\\--dump Dumps document to stdout.
\\ Defaults to false.
\\
\\--markdown Converts document in Markdown format and dumps it to stdout.
\\ Defaults to false.
\\
++ common_options ++
\\
\\serve command
@@ -328,9 +317,6 @@ fn inferMode(opt: []const u8) ?App.RunMode {
if (std.mem.eql(u8, opt, "--dump")) {
return .fetch;
}
if (std.mem.eql(u8, opt, "--markdown")) {
return .fetch;
}
if (std.mem.startsWith(u8, opt, "--") == false) {
return .fetch;
}
@@ -416,7 +402,6 @@ fn parseFetchArgs(
args: *std.process.ArgIterator,
) !Command.Fetch {
var dump: bool = false;
var markdown: bool = false;
var url: ?[]const u8 = null;
var common: Command.Common = .{};
@@ -425,10 +410,6 @@ fn parseFetchArgs(
dump = true;
continue;
}
if (std.mem.eql(u8, "--markdown", opt)) {
markdown = true;
continue;
}
if (try parseCommonArg(allocator, opt, args, &common)) {
continue;
@@ -454,7 +435,6 @@ fn parseFetchArgs(
return .{
.url = url.?,
.dump = dump,
.markdown = markdown,
.common = common,
};
}
@@ -542,7 +522,7 @@ test {
var test_wg: std.Thread.WaitGroup = .{};
test "tests:beforeAll" {
try parser.init();
try parser.init(std.testing.allocator);
log.opts.level = .err;
log.opts.format = .logfmt;

View File

@@ -337,7 +337,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
const env = self.env;
const isolate = env.isolate;
const Global = @TypeOf(global.*);
const templates = &self.env.templates;
var v8_context: v8.Context = blk: {
var temp_scope: v8.HandleScope = undefined;
@@ -352,6 +351,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// All the FunctionTemplates that we created and setup in Env.init
// are now going to get associated with our global instance.
const templates = &self.env.templates;
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
const class_name = v8.String.initUtf8(isolate, comptime classNameForStruct(Struct));
@@ -400,7 +400,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
};
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
// The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
// The main Context/Scope that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
var handle_scope: ?v8.HandleScope = null;
if (enter) {
@@ -463,38 +463,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
}
}
// Primitive attributes are set directly on the FunctionTemplate
// when we setup the environment. But we cannot set more complex
// types (v8 will crash).
//
// Plus, just to create more complex types, we always need a
// context, i.e. an Array has to have a Context to exist.
//
// As far as I can tell, getting the FunctionTemplate's object
// and setting values directly on it, for each context, is the
// way to do this.
inline for (Types, 0..) |s, i| {
const Struct = s.defaultValue().?;
inline for (@typeInfo(Struct).@"struct".decls) |declaration| {
const name = declaration.name;
if (comptime name[0] == '_') {
const value = @field(Struct, name);
if (comptime isComplexAttributeType(@typeInfo(@TypeOf(value)))) {
const js_obj = templates[i].getFunction(v8_context).toObject();
const js_name = v8.String.initUtf8(isolate, name[1..]).toName();
const js_val = try js_context.zigValueToJs(value);
if (!js_obj.setValue(v8_context, js_name, js_val)) {
log.fatal(.app, "set class attribute", .{
.@"struct" = @typeName(Struct),
.name = name,
});
}
}
}
}
}
_ = try js_context._mapZigInstanceToJs(v8_context.getGlobal(), global);
return js_context;
}
@@ -1190,7 +1158,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
}
if (!js_value.isArray()) {
return .{ .invalid = {} };
return error.InvalidArgument;
}
// This can get tricky.
@@ -1259,25 +1227,12 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// const referrer_module = if (referrer) |ref| v8.Module{ .handle = ref } else null;
const module_loader = self.module_loader;
const source = module_loader.func(module_loader.ptr, specifier) catch |err| {
log.err(.js, "resolve module fetch", .{
.err = err,
.specifier = specifier,
});
log.err(.js, "resolve module fetch error", .{ .specifier = specifier, .err = err });
return null;
} orelse return null;
var try_catch: TryCatch = undefined;
try_catch.init(self);
defer try_catch.deinit();
const m = compileModule(self.isolate, source, specifier) catch |err| {
log.err(.js, "resolve module compile", .{
.specifier = specifier,
.stack = try_catch.stack(self.context_arena) catch null,
.src = try_catch.sourceLine(self.context_arena) catch "err",
.line = try_catch.sourceLineNumber() orelse 0,
.exception = (try_catch.exception(self.context_arena) catch @errorName(err)) orelse @errorName(err),
});
log.err(.js, "resolve module compile error", .{ .specifier = specifier, .err = err });
return null;
};
return m.handle;
@@ -1302,16 +1257,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
exception: []const u8,
};
pub fn getName(self: *const Function, allocator: Allocator) ![]const u8 {
const name = self.func.castToFunction().getName();
return valueToString(allocator, name, self.js_context.isolate, self.js_context.v8_context);
}
pub fn setName(self: *const Function, name: []const u8) void {
const v8_name = v8.String.initUtf8(self.js_context.isolate, name);
self.func.castToFunction().setName(v8_name);
}
pub fn withThis(self: *const Function, value: anytype) !Function {
const this_obj = if (@TypeOf(value) == JsObject)
value.js_obj
@@ -1326,33 +1271,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
};
}
pub fn newInstance(self: *const Function, result: *Result) !JsObject {
const context = self.js_context;
var try_catch: TryCatch = undefined;
try_catch.init(context);
defer try_catch.deinit();
// This creates a new instance using this Function as a constructor.
// This returns a generic Object
const js_obj = self.func.castToFunction().initInstance(context.v8_context, &.{}) orelse {
if (try_catch.hasCaught()) {
const allocator = context.call_arena;
result.stack = try_catch.stack(allocator) catch null;
result.exception = (try_catch.exception(allocator) catch "???") orelse "???";
} else {
result.stack = null;
result.exception = "???";
}
return error.JsConstructorFailed;
};
return .{
.js_context = context,
.js_obj = js_obj,
};
}
pub fn call(self: *const Function, comptime T: type, args: anytype) !T {
return self.callWithThis(T, self.getThis(), args);
}
@@ -1532,11 +1450,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
.js_obj = array.castTo(v8.Object),
};
}
pub fn constructorName(self: JsObject, allocator: Allocator) ![]const u8 {
const str = try self.js_obj.getConstructorName();
return jsStringToZig(allocator, str, self.js_context.isolate);
}
};
// This only exists so that we know whether a function wants the opaque
@@ -1559,10 +1472,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
pub fn set(self: JsThis, key: []const u8, value: anytype, opts: JsObject.SetOpts) !void {
return self.obj.set(key, value, opts);
}
pub fn constructorName(self: JsThis, allocator: Allocator) ![]const u8 {
return try self.obj.constructorName(allocator);
}
};
pub const TryCatch = struct {
@@ -1592,20 +1501,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
return try valueToString(allocator, s, js_context.isolate, js_context.v8_context);
}
// the caller needs to deinit the string returned
pub fn sourceLine(self: TryCatch, allocator: Allocator) !?[]const u8 {
const js_context = self.js_context;
const msg = self.inner.getMessage() orelse return null;
const sl = msg.getSourceLine(js_context.v8_context) orelse return null;
return try jsStringToZig(allocator, sl, js_context.isolate);
}
pub fn sourceLineNumber(self: TryCatch) ?u32 {
const js_context = self.js_context;
const msg = self.inner.getMessage() orelse return null;
return msg.getLineNumber(js_context.v8_context);
}
// a shorthand method to return either the entire stack message
// or just the exception message
// - in Debug mode return the stack if available
@@ -1841,9 +1736,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
if (comptime name[0] == '_') {
switch (@typeInfo(@TypeOf(@field(Struct, name)))) {
.@"fn" => generateMethod(Struct, name, isolate, template_proto),
else => |ti| if (!comptime isComplexAttributeType(ti)) {
generateAttribute(Struct, name, isolate, template, template_proto);
},
else => generateAttribute(Struct, name, isolate, template, template_proto),
}
} else if (comptime std.mem.startsWith(u8, name, "get_")) {
generateProperty(Struct, name[4..], isolate, template_proto);
@@ -1891,7 +1784,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// a constructor function, we'll return an error.
if (@hasDecl(Struct, "constructor") == false) {
const iso = caller.isolate;
const js_exception = iso.throwException(createException(iso, "Illegal Constructor"));
const js_exception = iso.throwException(createException(iso, "illegal constructor"));
info.getReturnValue().set(js_exception);
return;
}
@@ -1964,7 +1857,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// apply it both to the type itself
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
// and to instances of the type
// andto instances of the type
template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
}
@@ -1983,7 +1876,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
defer caller.deinit();
const named_function = comptime NamedFunction.init(Struct, "get_" ++ name);
caller.method(Struct, named_function, info) catch |err| {
caller.getter(Struct, named_function, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
@@ -1998,13 +1891,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
const setter_callback = v8.FunctionTemplate.initCallback(isolate, struct {
fn callback(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
std.debug.assert(info.length() == 1);
var caller = Caller(Self, State).init(info);
defer caller.deinit();
std.debug.assert(info.length() == 1);
const js_value = info.getArg(0);
const named_function = comptime NamedFunction.init(Struct, "set_" ++ name);
caller.method(Struct, named_function, info) catch |err| {
caller.setter(Struct, named_function, js_value, info) catch |err| {
caller.handleError(Struct, named_function, err, info);
};
}
@@ -2430,20 +2323,6 @@ fn isEmpty(comptime T: type) bool {
return @typeInfo(T) != .@"opaque" and @sizeOf(T) == 0 and @hasDecl(T, "js_legacy_factory") == false;
}
// Attributes that return a primitive type are setup directly on the
// FunctionTemplate when the Env is setup. More complex types need a v8.Context
// and cannot be set directly on the FunctionTemplate.
// We default to saying types are primitives because that's mostly what
// we have. If we add a new complex type that isn't explictly handled here,
// we'll get a compiler error in simpleZigValueToJs, and can then explicitly
// add the type here.
fn isComplexAttributeType(ti: std.builtin.Type) bool {
return switch (ti) {
.array => true,
else => false,
};
}
// Responsible for calling Zig functions from JS invokations. This could
// probably just contained in ExecutionWorld, but having this specific logic, which
// is somewhat repetitive between constructors, functions, getters, etc contained
@@ -2545,6 +2424,66 @@ fn Caller(comptime E: type, comptime State: type) type {
info.getReturnValue().set(try js_context.zigValueToJs(res));
}
fn getter(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, info: v8.FunctionCallbackInfo) !void {
const js_context = self.js_context;
const func = @field(Struct, named_function.name);
const Getter = @TypeOf(func);
if (@typeInfo(Getter).@"fn".return_type == null) {
@compileError(@typeName(Struct) ++ " has a getter without a return type: " ++ @typeName(Getter));
}
var args: ParamterTypes(Getter) = undefined;
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
switch (arg_fields.len) {
0 => {}, // getters _can_ be parameterless
1, 2 => {
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
comptime assertSelfReceiver(Struct, named_function);
@field(args, "0") = zig_instance;
if (comptime arg_fields.len == 2) {
comptime assertIsStateArg(Struct, named_function, 1);
@field(args, "1") = js_context.state;
}
},
else => @compileError(named_function.full_name + " has too many parmaters: " ++ @typeName(named_function.func)),
}
const res = @call(.auto, func, args);
info.getReturnValue().set(try js_context.zigValueToJs(res));
}
fn setter(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, js_value: v8.Value, info: v8.FunctionCallbackInfo) !void {
const js_context = self.js_context;
const func = @field(Struct, named_function.name);
comptime assertSelfReceiver(Struct, named_function);
const zig_instance = try E.typeTaggedAnyOpaque(named_function, *Receiver(Struct), info.getThis());
const Setter = @TypeOf(func);
var args: ParamterTypes(Setter) = undefined;
const arg_fields = @typeInfo(@TypeOf(args)).@"struct".fields;
switch (arg_fields.len) {
0 => unreachable, // assertSelfReceiver make sure of this
1 => @compileError(named_function.full_name ++ " only has 1 parameter"),
2, 3 => {
@field(args, "0") = zig_instance;
@field(args, "1") = try js_context.jsValueToZig(named_function, arg_fields[1].type, js_value);
if (comptime arg_fields.len == 3) {
comptime assertIsStateArg(Struct, named_function, 2);
@field(args, "2") = js_context.state;
}
},
else => @compileError(named_function.full_name ++ " setter with more than 3 parameters, why?"),
}
if (@typeInfo(Setter).@"fn".return_type) |return_type| {
if (@typeInfo(return_type) == .error_union) {
_ = try @call(.auto, func, args);
return;
}
}
_ = @call(.auto, func, args);
}
fn getIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, idx: u32, info: v8.PropertyCallbackInfo) !u8 {
const js_context = self.js_context;
const func = @field(Struct, named_function.name);
@@ -2657,14 +2596,19 @@ fn Caller(comptime E: type, comptime State: type) type {
if (comptime builtin.mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
if (log.enabled(.js, .warn)) {
logFunctionCallError(self.call_arena, self.isolate, self.v8_context, err, named_function.full_name, info);
const args_dump = self.serializeFunctionArgs(info) catch "failed to serialize args";
log.warn(.js, "function call error", .{
.name = named_function.full_name,
.err = err,
.args = args_dump,
.stack = stackForLogs(self.call_arena, isolate) catch |err1| @errorName(err1),
});
}
}
var js_err: ?v8.Value = switch (err) {
error.InvalidArgument => createTypeException(isolate, "invalid argument"),
error.OutOfMemory => createException(isolate, "out of memory"),
error.IllegalConstructor => createException(isolate, "Illegal Contructor"),
else => blk: {
const func = @field(Struct, named_function.name);
const return_type = @typeInfo(@TypeOf(func)).@"fn".return_type orelse {
@@ -2726,7 +2670,6 @@ fn Caller(comptime E: type, comptime State: type) type {
// Does the error we want to return belong to the custom exeception's ErrorSet
fn isErrorSetException(comptime Exception: type, err: anytype) bool {
const Entry = std.meta.Tuple(&.{ []const u8, void });
const error_set = @typeInfo(Exception.ErrorSet).error_set.?;
const entries = comptime blk: {
var kv: [error_set.len]Entry = undefined;
@@ -2778,8 +2721,8 @@ fn Caller(comptime E: type, comptime State: type) type {
// a JS argument
if (comptime isJsThis(params[params.len - 1].type.?)) {
@field(args, std.fmt.comptimePrint("{d}", .{params.len - 1 + offset})) = .{ .obj = .{
.js_context = js_context,
.js_obj = info.getThis(),
.executor = self.executor,
} };
// AND the 2nd last parameter is state
@@ -2865,6 +2808,28 @@ fn Caller(comptime E: type, comptime State: type) type {
const Const_State = if (ti == .pointer) *const ti.pointer.child else State;
return T == State or T == Const_State;
}
fn serializeFunctionArgs(self: *const Self, info: anytype) ![]const u8 {
const isolate = self.isolate;
const v8_context = self.v8_context;
const arena = self.call_arena;
const separator = log.separator();
const js_parameter_count = info.length();
var arr: std.ArrayListUnmanaged(u8) = .{};
for (0..js_parameter_count) |i| {
const js_value = info.getArg(@intCast(i));
const value_string = try valueToDetailString(arena, js_value, isolate, v8_context);
const value_type = try jsStringToZig(arena, try js_value.typeOf(isolate), isolate);
try std.fmt.format(arr.writer(arena), "{s}{d}: {s} ({s})", .{
separator,
i + 1,
value_string,
value_type,
});
}
return arr.items;
}
};
}
@@ -3305,37 +3270,6 @@ const NamedFunction = struct {
}
};
// This is extracted to speed up compilation. When left inlined in handleError,
// this can add as much as 10 seconds of compilation time.
fn logFunctionCallError(arena: Allocator, isolate: v8.Isolate, context: v8.Context, err: anyerror, function_name: []const u8, info: v8.FunctionCallbackInfo) void {
const args_dump = serializeFunctionArgs(arena, isolate, context, info) catch "failed to serialize args";
log.warn(.js, "function call error", .{
.name = function_name,
.err = err,
.args = args_dump,
.stack = stackForLogs(arena, isolate) catch |err1| @errorName(err1),
});
}
fn serializeFunctionArgs(arena: Allocator, isolate: v8.Isolate, context: v8.Context, info: v8.FunctionCallbackInfo) ![]const u8 {
const separator = log.separator();
const js_parameter_count = info.length();
var arr: std.ArrayListUnmanaged(u8) = .{};
for (0..js_parameter_count) |i| {
const js_value = info.getArg(@intCast(i));
const value_string = try valueToDetailString(arena, js_value, isolate, context);
const value_type = try jsStringToZig(arena, try js_value.typeOf(isolate), isolate);
try std.fmt.format(arr.writer(arena), "{s}{d}: {s} ({s})", .{
separator,
i + 1,
value_string,
value_type,
});
}
return arr.items;
}
// This is called from V8. Whenever the v8 inspector has to describe a value
// it'll call this function to gets its [optional] subtype - which, from V8's
// point of view, is an arbitrary string.

125
src/str/parser.zig Normal file
View File

@@ -0,0 +1,125 @@
// 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")));
}

View File

@@ -211,14 +211,16 @@ pub const Document = struct {
arena: std.heap.ArenaAllocator,
pub fn init(html: []const u8) !Document {
var arena = std.heap.ArenaAllocator.init(allocator);
parser.deinit();
try parser.init();
try parser.init(arena.allocator());
var fbs = std.io.fixedBufferStream(html);
const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8");
const Elements = @import("browser/html/elements.zig");
const html_doc = try parser.documentHTMLParse(fbs.reader(), "utf-8", &Elements.createElement);
return .{
.arena = std.heap.ArenaAllocator.init(allocator),
.arena = arena,
.doc = html_doc,
};
}