mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3575f45ac0 | ||
|
|
326851ed6f | ||
|
|
5dcc3db36b | ||
|
|
c5d49a9d34 | ||
|
|
ef9f828d35 | ||
|
|
c691764205 | ||
|
|
2c940d4fd6 | ||
|
|
54bd55d45d | ||
|
|
0b846b15b1 | ||
|
|
269eb7e154 | ||
|
|
97bc19e4ae | ||
|
|
2656cc7842 | ||
|
|
ba94818415 | ||
|
|
ac759a6eed | ||
|
|
1839b346a6 | ||
|
|
c1ffe7f8e6 | ||
|
|
833b4d10bd | ||
|
|
ce98c336c9 | ||
|
|
d05619990a | ||
|
|
8033e41d4a | ||
|
|
60f4eab759 | ||
|
|
d7656ea985 | ||
|
|
e402998577 | ||
|
|
073f75efa3 | ||
|
|
da414f7eb3 | ||
|
|
270b89830a | ||
|
|
74ce7ca416 | ||
|
|
3f4338cb51 | ||
|
|
30ee41fd0e | ||
|
|
4965fec55c | ||
|
|
18dff8455c | ||
|
|
fe16f06aee | ||
|
|
48c1c05a93 | ||
|
|
38dee1166d | ||
|
|
0c6fc68eae | ||
|
|
223611d89e | ||
|
|
6f5141d5fb | ||
|
|
a6ac7d9c4e | ||
|
|
9b35736be3 | ||
|
|
9f54cb35f4 | ||
|
|
329bffb127 | ||
|
|
e2542f41b5 | ||
|
|
efc7b9d4a5 | ||
|
|
72915760c4 | ||
|
|
e9d7a946c5 | ||
|
|
714e5e0456 | ||
|
|
26e8642aca | ||
|
|
68dfb4ee86 | ||
|
|
f1ff789334 | ||
|
|
1f45d5b8e4 | ||
|
|
c20052f314 | ||
|
|
c28d87d59c | ||
|
|
237ddcba9a | ||
|
|
eadb5b6461 |
@@ -605,6 +605,7 @@ pub const Parser = struct {
|
||||
.after, .backdrop, .before, .cue, .first_letter => return .{ .pseudo_element = pseudo_class },
|
||||
.first_line, .grammar_error, .marker, .placeholder => return .{ .pseudo_element = pseudo_class },
|
||||
.selection, .spelling_error => return .{ .pseudo_element = pseudo_class },
|
||||
.modal => return .{ .pseudo_element = pseudo_class },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ pub const PseudoClass = enum {
|
||||
placeholder,
|
||||
selection,
|
||||
spelling_error,
|
||||
modal,
|
||||
|
||||
pub const Error = error{
|
||||
InvalidPseudoClass,
|
||||
@@ -154,6 +155,7 @@ pub const PseudoClass = enum {
|
||||
if (std.ascii.eqlIgnoreCase(s, "placeholder")) return .placeholder;
|
||||
if (std.ascii.eqlIgnoreCase(s, "selection")) return .selection;
|
||||
if (std.ascii.eqlIgnoreCase(s, "spelling-error")) return .spelling_error;
|
||||
if (std.ascii.eqlIgnoreCase(s, "modal")) return .modal;
|
||||
return Error.InvalidPseudoClass;
|
||||
}
|
||||
};
|
||||
|
||||
42
src/browser/cssom/css_rule.zig
Normal file
42
src/browser/cssom/css_rule.zig
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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,
|
||||
};
|
||||
60
src/browser/cssom/css_rule_list.zig
Normal file
60
src/browser/cssom/css_rule_list.zig
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -20,15 +20,9 @@ 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),
|
||||
@@ -85,7 +79,7 @@ pub const CSSStyleDeclaration = struct {
|
||||
return self.order.items.len;
|
||||
}
|
||||
|
||||
pub fn get_parentRule() ?CSSRule {
|
||||
pub fn get_parentRule(_: *const CSSStyleDeclaration) ?CSSRule {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
91
src/browser/cssom/css_stylesheet.zig
Normal file
91
src/browser/cssom/css_stylesheet.zig
Normal file
@@ -0,0 +1,91 @@
|
||||
// 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" },
|
||||
}, .{});
|
||||
}
|
||||
30
src/browser/cssom/cssom.zig
Normal file
30
src/browser/cssom/cssom.zig
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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,
|
||||
};
|
||||
55
src/browser/cssom/stylesheet.zig
Normal file
55
src/browser/cssom/stylesheet.zig
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const log = @import("../../log.zig");
|
||||
const parser = @import("../netsurf.zig");
|
||||
const Page = @import("../page.zig").Page;
|
||||
|
||||
@@ -120,9 +121,28 @@ pub const Document = struct {
|
||||
return try Element.toInterface(e);
|
||||
}
|
||||
|
||||
pub fn _createElement(self: *parser.Document, tag_name: []const u8) !ElementUnion {
|
||||
const CreateElementResult = union(enum) {
|
||||
element: ElementUnion,
|
||||
custom: Env.JsObject,
|
||||
};
|
||||
|
||||
pub fn _createElement(self: *parser.Document, tag_name: []const u8, page: *Page) !CreateElementResult {
|
||||
const custom_element = page.window.custom_elements._get(tag_name) orelse {
|
||||
const e = try parser.documentCreateElement(self, tag_name);
|
||||
return try Element.toInterface(e);
|
||||
return .{ .element = try Element.toInterface(e) };
|
||||
};
|
||||
|
||||
var result: Env.Function.Result = undefined;
|
||||
const js_obj = custom_element.newInstance(&result) catch |err| {
|
||||
log.fatal(.user_script, "newInstance error", .{
|
||||
.err = result.exception,
|
||||
.stack = result.stack,
|
||||
.tag_name = tag_name,
|
||||
.source = "createElement",
|
||||
});
|
||||
return err;
|
||||
};
|
||||
return .{ .custom = js_obj };
|
||||
}
|
||||
|
||||
pub fn _createElementNS(self: *parser.Document, ns: []const u8, tag_name: []const u8) !ElementUnion {
|
||||
|
||||
@@ -28,6 +28,8 @@ 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,
|
||||
@@ -44,4 +46,6 @@ pub const Interfaces = .{
|
||||
DOMParser,
|
||||
TreeWalker,
|
||||
NodeFilter,
|
||||
Performance,
|
||||
PerformanceObserver,
|
||||
};
|
||||
|
||||
@@ -43,6 +43,10 @@ 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 {
|
||||
@@ -369,7 +373,16 @@ 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 };
|
||||
return DOMRect{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = 0,
|
||||
.height = 0,
|
||||
.bottom = 0,
|
||||
.right = 0,
|
||||
.top = 0,
|
||||
.left = 0,
|
||||
};
|
||||
}
|
||||
return page.renderer.getRect(self);
|
||||
}
|
||||
|
||||
34
src/browser/dom/performance_observer.zig
Normal file
34
src/browser/dom/performance_observer.zig
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -22,7 +22,7 @@ const WebApis = struct {
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
@import("crypto/crypto.zig").Crypto,
|
||||
@import("console/console.zig").Console,
|
||||
@import("cssom/css_style_declaration.zig").Interfaces,
|
||||
@import("cssom/cssom.zig").Interfaces,
|
||||
@import("dom/dom.zig").Interfaces,
|
||||
@import("encoding/text_encoder.zig").Interfaces,
|
||||
@import("events/event.zig").Interfaces,
|
||||
@@ -33,6 +33,7 @@ const WebApis = struct {
|
||||
@import("xhr/xhr.zig").Interfaces,
|
||||
@import("xhr/form_data.zig").Interfaces,
|
||||
@import("xmlserializer/xmlserializer.zig").Interfaces,
|
||||
@import("webcomponents/webcomponents.zig").Interfaces,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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 });
|
||||
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true, .is_http = false });
|
||||
return buf.items;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,10 @@ 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;
|
||||
}
|
||||
@@ -333,6 +337,8 @@ 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(&.{
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
const std = @import("std");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
const generate = @import("../../runtime/generate.zig");
|
||||
@@ -112,6 +113,10 @@ pub const HTMLElement = struct {
|
||||
pub const prototype = *Element;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
|
||||
pub fn get_style(e: *parser.ElementHTML, page: *Page) !*CSSStyleDeclaration {
|
||||
const state = try page.getOrCreateNodeState(@ptrCast(e));
|
||||
return &state.style;
|
||||
@@ -174,6 +179,10 @@ pub const HTMLMediaElement = struct {
|
||||
pub const Self = parser.MediaElement;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
// HTML elements
|
||||
@@ -183,6 +192,10 @@ pub const HTMLUnknownElement = struct {
|
||||
pub const Self = parser.Unknown;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
// https://html.spec.whatwg.org/#the-a-element
|
||||
@@ -191,6 +204,10 @@ pub const HTMLAnchorElement = struct {
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
|
||||
pub fn get_target(self: *parser.Anchor) ![]const u8 {
|
||||
return try parser.anchorGetTarget(self);
|
||||
}
|
||||
@@ -428,144 +445,240 @@ pub const HTMLAppletElement = struct {
|
||||
pub const Self = parser.Applet;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLAreaElement = struct {
|
||||
pub const Self = parser.Area;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLAudioElement = struct {
|
||||
pub const Self = parser.Audio;
|
||||
pub const prototype = *HTMLMediaElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLBRElement = struct {
|
||||
pub const Self = parser.BR;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLBaseElement = struct {
|
||||
pub const Self = parser.Base;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLBodyElement = struct {
|
||||
pub const Self = parser.Body;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLButtonElement = struct {
|
||||
pub const Self = parser.Button;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLCanvasElement = struct {
|
||||
pub const Self = parser.Canvas;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLDListElement = struct {
|
||||
pub const Self = parser.DList;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLDataElement = struct {
|
||||
pub const Self = parser.Data;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLDataListElement = struct {
|
||||
pub const Self = parser.DataList;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLDialogElement = struct {
|
||||
pub const Self = parser.Dialog;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLDirectoryElement = struct {
|
||||
pub const Self = parser.Directory;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLDivElement = struct {
|
||||
pub const Self = parser.Div;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLEmbedElement = struct {
|
||||
pub const Self = parser.Embed;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLFieldSetElement = struct {
|
||||
pub const Self = parser.FieldSet;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLFontElement = struct {
|
||||
pub const Self = parser.Font;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLFrameElement = struct {
|
||||
pub const Self = parser.Frame;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLFrameSetElement = struct {
|
||||
pub const Self = parser.FrameSet;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLHRElement = struct {
|
||||
pub const Self = parser.HR;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLHeadElement = struct {
|
||||
pub const Self = parser.Head;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLHeadingElement = struct {
|
||||
pub const Self = parser.Heading;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLHtmlElement = struct {
|
||||
pub const Self = parser.Html;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLIFrameElement = struct {
|
||||
pub const Self = parser.IFrame;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLImageElement = struct {
|
||||
@@ -573,6 +686,10 @@ pub const HTMLImageElement = struct {
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
|
||||
pub fn get_alt(self: *parser.Image) ![]const u8 {
|
||||
return try parser.imageGetAlt(self);
|
||||
}
|
||||
@@ -613,6 +730,7 @@ pub const HTMLImageElement = struct {
|
||||
pub const Factory = struct {
|
||||
pub const js_name = "Image";
|
||||
pub const subtype = .node;
|
||||
|
||||
pub const js_legacy_factory = true;
|
||||
pub const prototype = *HTMLImageElement;
|
||||
|
||||
@@ -631,6 +749,10 @@ pub const HTMLInputElement = struct {
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
|
||||
pub fn get_defaultValue(self: *parser.Input) ![]const u8 {
|
||||
return try parser.inputGetDefaultValue(self);
|
||||
}
|
||||
@@ -719,114 +841,190 @@ pub const HTMLLIElement = struct {
|
||||
pub const Self = parser.LI;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLLabelElement = struct {
|
||||
pub const Self = parser.Label;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLLegendElement = struct {
|
||||
pub const Self = parser.Legend;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLLinkElement = struct {
|
||||
pub const Self = parser.Link;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLMapElement = struct {
|
||||
pub const Self = parser.Map;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLMetaElement = struct {
|
||||
pub const Self = parser.Meta;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLMeterElement = struct {
|
||||
pub const Self = parser.Meter;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLModElement = struct {
|
||||
pub const Self = parser.Mod;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLOListElement = struct {
|
||||
pub const Self = parser.OList;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLObjectElement = struct {
|
||||
pub const Self = parser.Object;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLOptGroupElement = struct {
|
||||
pub const Self = parser.OptGroup;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLOptionElement = struct {
|
||||
pub const Self = parser.Option;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLOutputElement = struct {
|
||||
pub const Self = parser.Output;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLParagraphElement = struct {
|
||||
pub const Self = parser.Paragraph;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLParamElement = struct {
|
||||
pub const Self = parser.Param;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLPictureElement = struct {
|
||||
pub const Self = parser.Picture;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLPreElement = struct {
|
||||
pub const Self = parser.Pre;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLProgressElement = struct {
|
||||
pub const Self = parser.Progress;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLQuoteElement = struct {
|
||||
pub const Self = parser.Quote;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
// https://html.spec.whatwg.org/#the-script-element
|
||||
@@ -835,6 +1033,10 @@ pub const HTMLScriptElement = struct {
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
|
||||
pub fn get_src(self: *parser.Script) !?[]const u8 {
|
||||
return try parser.elementGetAttribute(
|
||||
parser.scriptToElt(self),
|
||||
@@ -969,101 +1171,166 @@ pub const HTMLSourceElement = struct {
|
||||
pub const Self = parser.Source;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLSpanElement = struct {
|
||||
pub const Self = parser.Span;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLStyleElement = struct {
|
||||
pub const Self = parser.Style;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTableElement = struct {
|
||||
pub const Self = parser.Table;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTableCaptionElement = struct {
|
||||
pub const Self = parser.TableCaption;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTableCellElement = struct {
|
||||
pub const Self = parser.TableCell;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTableColElement = struct {
|
||||
pub const Self = parser.TableCol;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTableRowElement = struct {
|
||||
pub const Self = parser.TableRow;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTableSectionElement = struct {
|
||||
pub const Self = parser.TableSection;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTemplateElement = struct {
|
||||
pub const Self = parser.Template;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTextAreaElement = struct {
|
||||
pub const Self = parser.TextArea;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTimeElement = struct {
|
||||
pub const Self = parser.Time;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTitleElement = struct {
|
||||
pub const Self = parser.Title;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLTrackElement = struct {
|
||||
pub const Self = parser.Track;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLUListElement = struct {
|
||||
pub const Self = parser.UList;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub const HTMLVideoElement = struct {
|
||||
pub const Self = parser.Video;
|
||||
pub const prototype = *HTMLElement;
|
||||
pub const subtype = .node;
|
||||
|
||||
pub fn constructor(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
return constructHtmlElement(page, js_this);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn toInterface(comptime T: type, e: *parser.Element) !T {
|
||||
const elem: *align(@alignOf(*parser.Element)) parser.Element = @alignCast(e);
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(elem)));
|
||||
|
||||
return switch (tag) {
|
||||
.abbr, .acronym, .address, .article, .aside, .b, .basefont, .bdi, .bdo, .bgsound, .big, .center, .cite, .code, .dd, .details, .dfn, .dt, .em, .figcaption, .figure, .footer, .header, .hgroup, .i, .isindex, .keygen, .kbd, .main, .mark, .marquee, .menu, .menuitem, .nav, .nobr, .noframes, .noscript, .rp, .rt, .ruby, .s, .samp, .section, .small, .spacer, .strike, .strong, .sub, .summary, .sup, .tt, .u, .wbr, ._var => .{ .HTMLElement = @as(*parser.ElementHTML, @ptrCast(elem)) },
|
||||
.a => .{ .HTMLAnchorElement = @as(*parser.Anchor, @ptrCast(elem)) },
|
||||
@@ -1135,6 +1402,16 @@ pub fn toInterface(comptime T: type, e: *parser.Element) !T {
|
||||
};
|
||||
}
|
||||
|
||||
fn constructHtmlElement(page: *Page, js_this: Env.JsThis) !*parser.Element {
|
||||
const constructor_name = try js_this.constructorName(page.call_arena);
|
||||
if (!page.window.custom_elements.lookup.contains(constructor_name)) {
|
||||
return error.IllegalContructor;
|
||||
}
|
||||
|
||||
const el = try parser.documentCreateElement(@ptrCast(page.window.document), constructor_name);
|
||||
return el;
|
||||
}
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "Browser.HTML.Element" {
|
||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||
|
||||
@@ -24,7 +24,6 @@ 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,
|
||||
@@ -37,5 +36,5 @@ pub const Interfaces = .{
|
||||
History,
|
||||
Location,
|
||||
MediaQueryList,
|
||||
Performance,
|
||||
@import("screen.zig").Interfaces,
|
||||
};
|
||||
|
||||
109
src/browser/html/screen.zig
Normal file
109
src/browser/html/screen.zig
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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" },
|
||||
}, .{});
|
||||
}
|
||||
@@ -31,8 +31,10 @@ 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("performance.zig").Performance;
|
||||
const Performance = @import("../dom/performance.zig").Performance;
|
||||
const CSSStyleDeclaration = @import("../cssom/css_style_declaration.zig").CSSStyleDeclaration;
|
||||
const CustomElementRegistry = @import("../webcomponents/custom_element_registry.zig").CustomElementRegistry;
|
||||
const Screen = @import("screen.zig").Screen;
|
||||
|
||||
const storage = @import("../storage/storage.zig");
|
||||
|
||||
@@ -58,6 +60,8 @@ 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("");
|
||||
@@ -163,6 +167,14 @@ 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 });
|
||||
}
|
||||
@@ -337,20 +349,15 @@ test "Browser.HTML.Window" {
|
||||
// Note however that we in this test do not wait as the request is just send to the browser
|
||||
try runner.testCases(&.{
|
||||
.{
|
||||
\\ let start;
|
||||
\\ let start = 0;
|
||||
\\ function step(timestamp) {
|
||||
\\ if (start === undefined) {
|
||||
\\ start = timestamp;
|
||||
\\ }
|
||||
\\ const elapsed = timestamp - start;
|
||||
\\ if (elapsed < 2000) {
|
||||
\\ requestAnimationFrame(step);
|
||||
\\ }
|
||||
\\ }
|
||||
,
|
||||
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
|
||||
@@ -386,4 +393,19 @@ 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",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
|
||||
337
src/browser/markdown.zig
Normal file
337
src/browser/markdown.zig
Normal file
@@ -0,0 +1,337 @@
|
||||
// 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, ";
|
||||
// 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("&"),
|
||||
'<' => try writer.writeAll("<"),
|
||||
'>' => try writer.writeAll(">"),
|
||||
else => unreachable,
|
||||
}
|
||||
v = v[index + 1 ..];
|
||||
}
|
||||
}
|
||||
@@ -775,6 +775,17 @@ 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 {
|
||||
|
||||
@@ -22,6 +22,7 @@ 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;
|
||||
@@ -147,20 +148,45 @@ 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 file_src = blk: {
|
||||
const src = blk: {
|
||||
if (base) |_base| {
|
||||
break :blk try URL.stitch(self.arena, specifier, _base, .{});
|
||||
} else break :blk specifier;
|
||||
};
|
||||
|
||||
if (self.module_map.get(file_src)) |module| return module;
|
||||
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,
|
||||
});
|
||||
|
||||
const module = try self.fetchData(specifier, base);
|
||||
if (module) |_module| try self.module_map.putNoClobber(self.arena, file_src, _module);
|
||||
if (module) |_module| try self.module_map.putNoClobber(self.arena, src, _module);
|
||||
return module;
|
||||
}
|
||||
|
||||
@@ -217,7 +243,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 });
|
||||
var request = try self.newHTTPRequest(opts.method, &self.url, .{ .navigation = true, .is_http = true });
|
||||
defer request.deinit();
|
||||
|
||||
request.body = opts.body;
|
||||
@@ -352,8 +378,23 @@ pub const Page = struct {
|
||||
continue;
|
||||
}
|
||||
|
||||
const e = parser.nodeToElement(next.?);
|
||||
const current = next.?;
|
||||
|
||||
const e = parser.nodeToElement(current);
|
||||
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
|
||||
|
||||
// if (tag == .undef) {
|
||||
// const tag_name = try parser.nodeLocalName(@ptrCast(e));
|
||||
// const custom_elements = &self.window.custom_elements;
|
||||
// if (custom_elements._get(tag_name)) |construct| {
|
||||
// try construct.printFunc();
|
||||
// // This is just here for testing for now.
|
||||
// // var result: Env.Function.Result = undefined;
|
||||
// // _ = try construct.newInstance(*parser.Element, &result);
|
||||
// log.info(.browser, "Registered WebComponent Found", .{ .element_name = tag_name });
|
||||
// }
|
||||
// }
|
||||
|
||||
if (tag != .script) {
|
||||
// ignore non-js script.
|
||||
continue;
|
||||
@@ -446,9 +487,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 {
|
||||
@@ -492,12 +533,23 @@ pub const Page = struct {
|
||||
var origin_url = &self.url;
|
||||
const url = try origin_url.resolve(arena, res_src);
|
||||
|
||||
log.debug(.http, "fetching script", .{ .url = url });
|
||||
errdefer |err| log.err(.http, "fetch error", .{ .err = err, .url = url });
|
||||
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,
|
||||
});
|
||||
|
||||
var request = try self.newHTTPRequest(.GET, &url, .{
|
||||
.origin_uri = &origin_url.uri,
|
||||
.navigation = false,
|
||||
.is_http = true,
|
||||
});
|
||||
defer request.deinit();
|
||||
|
||||
@@ -505,7 +557,8 @@ pub const Page = struct {
|
||||
var header = response.header;
|
||||
try self.session.cookie_jar.populateFromResponse(&url.uri, &header);
|
||||
|
||||
if (header.status < 200 or header.status > 299) {
|
||||
status_code = header.status;
|
||||
if (status_code < 200 or status_code > 299) {
|
||||
return error.BadStatusCode;
|
||||
}
|
||||
|
||||
@@ -523,7 +576,7 @@ pub const Page = struct {
|
||||
|
||||
log.info(.http, "fetch complete", .{
|
||||
.url = url,
|
||||
.status = header.status,
|
||||
.status = status_code,
|
||||
.content_length = arr.items.len,
|
||||
});
|
||||
return arr.items;
|
||||
@@ -986,6 +1039,9 @@ 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: {
|
||||
|
||||
@@ -62,20 +62,38 @@ 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 = @floatFromInt(x),
|
||||
.y = 0.0,
|
||||
.width = 1.0,
|
||||
.height = 1.0,
|
||||
.x = _x,
|
||||
.y = y,
|
||||
.width = w,
|
||||
.height = h,
|
||||
.left = _x,
|
||||
.top = y,
|
||||
.right = _x + w,
|
||||
.bottom = y + h,
|
||||
};
|
||||
}
|
||||
|
||||
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 = 0.0,
|
||||
.y = 0.0,
|
||||
.width = @floatFromInt(self.width()),
|
||||
.height = @floatFromInt(self.height()),
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = w,
|
||||
.height = h,
|
||||
.left = x,
|
||||
.top = y,
|
||||
.right = x + w,
|
||||
.bottom = y + h,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ pub const LookupOpts = struct {
|
||||
request_time: ?i64 = null,
|
||||
origin_uri: ?*const Uri = null,
|
||||
navigation: bool = true,
|
||||
is_http: bool,
|
||||
};
|
||||
|
||||
pub const Jar = struct {
|
||||
@@ -32,6 +33,13 @@ 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,
|
||||
@@ -59,87 +67,33 @@ 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_path = target_uri.path.percent_encoded;
|
||||
const target_host = (target_uri.host orelse return error.InvalidURI).percent_encoded;
|
||||
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 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();
|
||||
removeExpired(self, opts.request_time);
|
||||
|
||||
var first = true;
|
||||
while (i < cookies.len) {
|
||||
const cookie = &cookies[i];
|
||||
for (self.cookies.items) |*cookie| {
|
||||
if (!cookie.appliesTo(&target, same_site, opts.navigation, opts.is_http)) continue;
|
||||
|
||||
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;
|
||||
@@ -173,47 +127,9 @@ 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 <= now;
|
||||
return ce <= @as(f64, @floatFromInt(now));
|
||||
}
|
||||
|
||||
fn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool {
|
||||
@@ -256,12 +172,12 @@ pub const Cookie = struct {
|
||||
arena: ArenaAllocator,
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
path: []const u8,
|
||||
domain: []const u8,
|
||||
expires: ?i64,
|
||||
secure: bool,
|
||||
http_only: bool,
|
||||
same_site: SameSite,
|
||||
path: []const u8,
|
||||
expires: ?f64,
|
||||
secure: bool = false,
|
||||
http_only: bool = false,
|
||||
same_site: SameSite = .none,
|
||||
|
||||
const SameSite = enum {
|
||||
strict,
|
||||
@@ -292,9 +208,6 @@ 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) {
|
||||
@@ -313,7 +226,7 @@ pub const Cookie = struct {
|
||||
var secure: ?bool = null;
|
||||
var max_age: ?i64 = null;
|
||||
var http_only: ?bool = null;
|
||||
var expires: ?DateTime = null;
|
||||
var expires: ?[]const u8 = null;
|
||||
var same_site: ?Cookie.SameSite = null;
|
||||
|
||||
var it = std.mem.splitScalar(u8, rest, ';');
|
||||
@@ -339,37 +252,13 @@ pub const Cookie = struct {
|
||||
samesite,
|
||||
}, std.ascii.lowerString(&scrap, key_string)) orelse continue;
|
||||
|
||||
var value = if (sep == attribute.len) "" else trim(attribute[sep + 1 ..]);
|
||||
const value = if (sep == attribute.len) "" else trim(attribute[sep + 1 ..]);
|
||||
switch (key) {
|
||||
.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;
|
||||
},
|
||||
.path => path = value,
|
||||
.domain => domain = value,
|
||||
.secure => secure = true,
|
||||
.@"max-age" => max_age = std.fmt.parseInt(i64, value, 10) catch continue,
|
||||
.expires => expires = DateTime.parse(value, .rfc822) catch continue,
|
||||
.expires => expires = value,
|
||||
.httponly => http_only = true,
|
||||
.samesite => {
|
||||
same_site = std.meta.stringToEnum(Cookie.SameSite, std.ascii.lowerString(&scrap, value)) orelse continue;
|
||||
@@ -386,27 +275,28 @@ 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 = if (path) |p|
|
||||
try aa.dupe(u8, p)
|
||||
else
|
||||
try defaultPath(aa, uri.path.percent_encoded);
|
||||
const owned_path = try parsePath(aa, uri, path);
|
||||
const owned_domain = try parseDomain(aa, uri, domain);
|
||||
|
||||
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;
|
||||
var normalized_expires: ?f64 = null;
|
||||
if (max_age) |ma| {
|
||||
normalized_expires = std.time.timestamp() + ma;
|
||||
normalized_expires = @floatFromInt(std.time.timestamp() + ma);
|
||||
} else {
|
||||
// max age takes priority over expires
|
||||
if (expires) |e| {
|
||||
normalized_expires = e.sub(DateTime.now(), .seconds);
|
||||
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_});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,6 +313,100 @@ 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 ..];
|
||||
@@ -439,17 +423,77 @@ 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 "/";
|
||||
}
|
||||
const last = std.mem.lastIndexOfScalar(u8, document_path[1..], '/') orelse {
|
||||
return "/";
|
||||
pub const PreparedUri = struct {
|
||||
host: []const u8, // Percent encoded, lower case
|
||||
path: []const u8, // Percent encoded
|
||||
secure: bool, // True if scheme is https
|
||||
};
|
||||
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);
|
||||
@@ -463,6 +507,13 @@ 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 }{
|
||||
@@ -548,7 +599,7 @@ test "Jar: forRequest" {
|
||||
|
||||
{
|
||||
// test with no cookies
|
||||
try expectCookies("", &jar, test_uri, .{});
|
||||
try expectCookies("", &jar, test_uri, .{ .is_http = true });
|
||||
}
|
||||
|
||||
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global1=1"), now);
|
||||
@@ -562,97 +613,114 @@ 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, .{});
|
||||
try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .navigation = false });
|
||||
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 });
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -660,40 +728,6 @@ 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' });
|
||||
@@ -816,7 +850,8 @@ 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 - std.time.timestamp() }, null, "b;expires=Wed, 21 Oct 2030 07:28:00 GMT");
|
||||
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");
|
||||
// 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");
|
||||
}
|
||||
@@ -836,7 +871,7 @@ test "Cookie: parse all" {
|
||||
.http_only = true,
|
||||
.secure = true,
|
||||
.domain = ".lightpanda.io",
|
||||
.expires = std.time.timestamp() + 30,
|
||||
.expires = @floatFromInt(std.time.timestamp() + 30),
|
||||
}, "https://lightpanda.io/cms/users", "user-id=9000; HttpOnly; Max-Age=30; Secure; path=/; Domain=lightpanda.io");
|
||||
|
||||
try expectCookie(.{
|
||||
@@ -847,7 +882,7 @@ test "Cookie: parse all" {
|
||||
.secure = false,
|
||||
.domain = ".localhost",
|
||||
.same_site = .lax,
|
||||
.expires = std.time.timestamp() + 7200,
|
||||
.expires = @floatFromInt(std.time.timestamp() + 7200),
|
||||
}, "http://localhost:8000/login", "app_session=123; Max-Age=7200; path=/; domain=localhost; httponly; samesite=lax");
|
||||
}
|
||||
|
||||
@@ -874,7 +909,7 @@ const ExpectedCookie = struct {
|
||||
value: []const u8,
|
||||
path: []const u8,
|
||||
domain: []const u8,
|
||||
expires: ?i64 = null,
|
||||
expires: ?f64 = null,
|
||||
secure: bool = false,
|
||||
http_only: bool = false,
|
||||
same_site: Cookie.SameSite = .lax,
|
||||
@@ -893,7 +928,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);
|
||||
try testing.expectDelta(expected.expires, cookie.expires, 2.0);
|
||||
}
|
||||
|
||||
fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) !void {
|
||||
@@ -903,7 +938,10 @@ 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")) {
|
||||
try testing.expectDelta(expected.expires, cookie.expires, 1);
|
||||
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),
|
||||
}
|
||||
} else {
|
||||
try testing.expectEqual(@field(expected, f.name), @field(cookie, f.name));
|
||||
}
|
||||
|
||||
91
src/browser/webcomponents/custom_element_registry.zig
Normal file
91
src/browser/webcomponents/custom_element_registry.zig
Normal file
@@ -0,0 +1,91 @@
|
||||
// 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" },
|
||||
}, .{});
|
||||
}
|
||||
23
src/browser/webcomponents/webcomponents.zig
Normal file
23
src/browser/webcomponents/webcomponents.zig
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const CustomElementRegistry = @import("custom_element_registry.zig").CustomElementRegistry;
|
||||
|
||||
pub const Interfaces = .{
|
||||
CustomElementRegistry,
|
||||
};
|
||||
@@ -115,17 +115,24 @@ const EntryIterable = iterator.Iterable(kv.EntryIterator, "FormDataEntryIterator
|
||||
// TODO: handle disabled fieldsets
|
||||
fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !kv.List {
|
||||
const arena = page.arena;
|
||||
const collection = try parser.formGetCollection(form);
|
||||
const len = try parser.htmlCollectionGetLength(collection);
|
||||
|
||||
// Don't use libdom's formGetCollection (aka dom_html_form_element_get_elements)
|
||||
// It doesn't work with dynamically added elements, because their form
|
||||
// property doesn't get set. We should fix that.
|
||||
// However, even once fixed, there are other form-collection features we
|
||||
// probably want to implement (like disabled fieldsets), so we might want
|
||||
// to stick with our own walker even if fix libdom to properly support
|
||||
// dynamically added elements.
|
||||
const node_list = try @import("../dom/css.zig").querySelectorAll(arena, @alignCast(@ptrCast(form)), "input,select,button,textarea");
|
||||
const nodes = node_list.nodes.items;
|
||||
|
||||
var entries: kv.List = .{};
|
||||
try entries.ensureTotalCapacity(arena, len);
|
||||
try entries.ensureTotalCapacity(arena, nodes.len);
|
||||
|
||||
var submitter_included = false;
|
||||
const submitter_name_ = try getSubmitterName(submitter_);
|
||||
|
||||
for (0..len) |i| {
|
||||
const node = try parser.htmlCollectionItem(collection, @intCast(i));
|
||||
for (nodes) |node| {
|
||||
const element = parser.nodeToElement(node);
|
||||
|
||||
// must have a name
|
||||
@@ -181,10 +188,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
|
||||
submitter_included = true;
|
||||
}
|
||||
},
|
||||
else => {
|
||||
log.warn(.web_api, "unsupported form element", .{ .tag = @tagName(tag) });
|
||||
continue;
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +301,7 @@ test "Browser.FormData" {
|
||||
\\ <input type=submit name=s2 value=s2-v>
|
||||
\\ <input type=image name=i1 value=i1-v>
|
||||
\\ </form>
|
||||
\\ <input type=text name=abc value=123 form=form1>
|
||||
});
|
||||
defer runner.deinit();
|
||||
|
||||
@@ -356,6 +361,8 @@ test "Browser.FormData" {
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let form1 = document.getElementById('form1')", null },
|
||||
.{ "let input = document.createElement('input');", null },
|
||||
.{ "input.name = 'dyn'; input.value= 'dyn-v'; form1.appendChild(input);", null },
|
||||
.{ "let submit1 = document.getElementById('s1')", null },
|
||||
.{ "let f2 = new FormData(form1, submit1)", null },
|
||||
.{ "acc = '';", null },
|
||||
@@ -378,6 +385,7 @@ test "Browser.FormData" {
|
||||
\\mlt-2=water
|
||||
\\mlt-2=tea
|
||||
\\s1=s1-v
|
||||
\\dyn=dyn-v
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -475,6 +475,7 @@ 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) {
|
||||
|
||||
@@ -23,7 +23,6 @@ const json = std.json;
|
||||
const log = @import("../log.zig");
|
||||
const App = @import("../app.zig").App;
|
||||
const Env = @import("../browser/env.zig").Env;
|
||||
const asUint = @import("../str/parser.zig").asUint;
|
||||
const Browser = @import("../browser/browser.zig").Browser;
|
||||
const Session = @import("../browser/session.zig").Session;
|
||||
const Page = @import("../browser/page.zig").Page;
|
||||
@@ -182,41 +181,42 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
|
||||
switch (domain.len) {
|
||||
3 => switch (@as(u24, @bitCast(domain[0..3].*))) {
|
||||
asUint("DOM") => return @import("domains/dom.zig").processMessage(command),
|
||||
asUint("Log") => return @import("domains/log.zig").processMessage(command),
|
||||
asUint("CSS") => return @import("domains/css.zig").processMessage(command),
|
||||
asUint(u24, "DOM") => return @import("domains/dom.zig").processMessage(command),
|
||||
asUint(u24, "Log") => return @import("domains/log.zig").processMessage(command),
|
||||
asUint(u24, "CSS") => return @import("domains/css.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
4 => switch (@as(u32, @bitCast(domain[0..4].*))) {
|
||||
asUint("Page") => return @import("domains/page.zig").processMessage(command),
|
||||
asUint(u32, "Page") => return @import("domains/page.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
5 => switch (@as(u40, @bitCast(domain[0..5].*))) {
|
||||
asUint("Fetch") => return @import("domains/fetch.zig").processMessage(command),
|
||||
asUint("Input") => return @import("domains/input.zig").processMessage(command),
|
||||
asUint(u40, "Fetch") => return @import("domains/fetch.zig").processMessage(command),
|
||||
asUint(u40, "Input") => return @import("domains/input.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(domain[0..6].*))) {
|
||||
asUint("Target") => return @import("domains/target.zig").processMessage(command),
|
||||
asUint(u48, "Target") => return @import("domains/target.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
7 => switch (@as(u56, @bitCast(domain[0..7].*))) {
|
||||
asUint("Browser") => return @import("domains/browser.zig").processMessage(command),
|
||||
asUint("Runtime") => return @import("domains/runtime.zig").processMessage(command),
|
||||
asUint("Network") => return @import("domains/network.zig").processMessage(command),
|
||||
asUint(u56, "Browser") => return @import("domains/browser.zig").processMessage(command),
|
||||
asUint(u56, "Runtime") => return @import("domains/runtime.zig").processMessage(command),
|
||||
asUint(u56, "Network") => return @import("domains/network.zig").processMessage(command),
|
||||
asUint(u56, "Storage") => return @import("domains/storage.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
8 => switch (@as(u64, @bitCast(domain[0..8].*))) {
|
||||
asUint("Security") => return @import("domains/security.zig").processMessage(command),
|
||||
asUint(u64, "Security") => return @import("domains/security.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
9 => switch (@as(u72, @bitCast(domain[0..9].*))) {
|
||||
asUint("Emulation") => return @import("domains/emulation.zig").processMessage(command),
|
||||
asUint("Inspector") => return @import("domains/inspector.zig").processMessage(command),
|
||||
asUint(u72, "Emulation") => return @import("domains/emulation.zig").processMessage(command),
|
||||
asUint(u72, "Inspector") => return @import("domains/inspector.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
11 => switch (@as(u88, @bitCast(domain[0..11].*))) {
|
||||
asUint("Performance") => return @import("domains/performance.zig").processMessage(command),
|
||||
asUint(u88, "Performance") => return @import("domains/performance.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
@@ -320,6 +320,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
|
||||
inspector: Inspector,
|
||||
isolated_world: ?IsolatedWorld,
|
||||
http_proxy_before: ??std.Uri = null,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
@@ -374,6 +375,8 @@ 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 {
|
||||
@@ -696,6 +699,10 @@ const InputParams = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn asUint(comptime T: type, comptime string: []const u8) T {
|
||||
return @bitCast(string[0..string.len].*);
|
||||
}
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "cdp: invalid json" {
|
||||
var ctx = testing.context();
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
// 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 Allocator = std.mem.Allocator;
|
||||
const CdpStorage = @import("storage.zig");
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
@@ -28,6 +29,11 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
disable,
|
||||
setCacheDisabled,
|
||||
setExtraHTTPHeaders,
|
||||
deleteCookies,
|
||||
clearBrowserCookies,
|
||||
setCookie,
|
||||
setCookies,
|
||||
getCookies,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
@@ -35,6 +41,11 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +82,112 @@ 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 {
|
||||
@@ -235,3 +352,77 @@ 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 });
|
||||
}
|
||||
|
||||
301
src/cdp/domains/storage.zig
Normal file
301
src/cdp/domains/storage.zig
Normal file
@@ -0,0 +1,301 @@
|
||||
// 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",
|
||||
};
|
||||
@@ -66,11 +66,30 @@ 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,
|
||||
}, .{});
|
||||
|
||||
@@ -716,7 +716,7 @@ pub const Request = struct {
|
||||
}
|
||||
|
||||
fn newReader(self: *Request) Reader {
|
||||
return Reader.init(self._state, &self._keepalive);
|
||||
return Reader.init(self._state);
|
||||
}
|
||||
|
||||
// Does additional setup of the request for the firsts (i.e. non-redirect) call.
|
||||
@@ -879,12 +879,14 @@ pub const Request = struct {
|
||||
});
|
||||
}
|
||||
|
||||
fn requestCompleted(self: *Request, response: ResponseHeader) void {
|
||||
fn requestCompleted(self: *Request, response: ResponseHeader, can_keepalive: bool) 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,
|
||||
@@ -1111,14 +1113,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.request.requestCompleted(self.reader.response, self.reader.keepalive);
|
||||
self.deinit();
|
||||
return;
|
||||
},
|
||||
.done => {
|
||||
const redirect = self.redirect orelse {
|
||||
var handler = self.handler;
|
||||
self.request.requestCompleted(self.reader.response);
|
||||
self.request.requestCompleted(self.reader.response, self.reader.keepalive);
|
||||
self.deinit();
|
||||
|
||||
// Emit the done chunk. We expect the caller to do
|
||||
@@ -1560,8 +1562,6 @@ 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 {
|
||||
// ref request.keepalive
|
||||
keepalive: *bool,
|
||||
// Wether, from the reader's point of view, this connection could be kept-alive
|
||||
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, keepalive: *bool) Reader {
|
||||
fn init(state: *State) Reader {
|
||||
return .{
|
||||
.pos = 0,
|
||||
.response = .{},
|
||||
.body_reader = null,
|
||||
.header_done = false,
|
||||
.keepalive = keepalive,
|
||||
.keepalive = false,
|
||||
.skip_current_header = false,
|
||||
.header_buf = state.header_buf,
|
||||
.arena = state.arena.allocator(),
|
||||
@@ -1835,7 +1835,6 @@ 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;
|
||||
}
|
||||
@@ -1930,7 +1929,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;
|
||||
}
|
||||
@@ -1945,7 +1944,7 @@ const Reader = struct {
|
||||
|
||||
if (response.get("connection")) |connection| {
|
||||
if (std.ascii.eqlIgnoreCase(connection, "close")) {
|
||||
self.keepalive.* = false;
|
||||
self.keepalive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2005,7 +2004,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;
|
||||
}
|
||||
@@ -2387,7 +2386,7 @@ pub const Response = struct {
|
||||
return data;
|
||||
}
|
||||
if (self._done) {
|
||||
self._request.requestCompleted(self.header);
|
||||
self._request.requestCompleted(self.header, self._reader.keepalive);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3170,7 +3169,7 @@ test "HttpClient: async tls no body" {
|
||||
}
|
||||
}
|
||||
|
||||
test "HttpClient: async tls with body x" {
|
||||
test "HttpClient: async tls with body" {
|
||||
defer testing.reset();
|
||||
for (0..5) |_| {
|
||||
var client = try testClient();
|
||||
@@ -3396,8 +3395,7 @@ const CaptureHandler = struct {
|
||||
|
||||
fn testReader(state: *State, res: *TestResponse, data: []const u8) !void {
|
||||
var status: u16 = 0;
|
||||
var keepalive = false;
|
||||
var r = Reader.init(state, &keepalive);
|
||||
var r = Reader.init(state);
|
||||
|
||||
// dupe it so that we have a mutable copy
|
||||
const owned = try testing.allocator.dupe(u8, data);
|
||||
|
||||
27
src/log.zig
27
src/log.zig
@@ -146,6 +146,16 @@ fn logTo(comptime scope: Scope, level: Level, comptime msg: []const u8, data: an
|
||||
}
|
||||
|
||||
fn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
|
||||
try logLogFmtPrefix(scope, level, msg, writer);
|
||||
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
|
||||
const key = " " ++ f.name ++ "=";
|
||||
try writer.writeAll(key);
|
||||
try writeValue(.logfmt, @field(data, f.name), writer);
|
||||
}
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
fn logLogFmtPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: anytype) !void {
|
||||
try writer.writeAll("$time=");
|
||||
try writer.print("{d}", .{timestamp()});
|
||||
|
||||
@@ -164,15 +174,20 @@ fn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data
|
||||
break :blk prefix ++ "\"" ++ msg ++ "\"";
|
||||
};
|
||||
try writer.writeAll(full_msg);
|
||||
}
|
||||
|
||||
fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
|
||||
try logPrettyPrefix(scope, level, msg, writer);
|
||||
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
|
||||
const key = " " ++ f.name ++ " = ";
|
||||
try writer.writeAll(key);
|
||||
try writeValue(.logfmt, @field(data, f.name), writer);
|
||||
try writeValue(.pretty, @field(data, f.name), writer);
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
|
||||
fn logPrettyPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: anytype) !void {
|
||||
if (scope == .console and level == .fatal and comptime std.mem.eql(u8, msg, "lightpanda")) {
|
||||
try writer.writeAll("\x1b[0;104mWARN ");
|
||||
} else {
|
||||
@@ -201,14 +216,6 @@ fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data
|
||||
try writer.print(" \x1b[0m[+{d}ms]", .{elapsed()});
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
|
||||
const key = " " ++ f.name ++ " = ";
|
||||
try writer.writeAll(key);
|
||||
try writeValue(.pretty, @field(data, f.name), writer);
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
pub fn writeValue(comptime format: Format, value: anytype, writer: anytype) !void {
|
||||
|
||||
22
src/main.zig
22
src/main.zig
@@ -103,7 +103,7 @@ fn run(alloc: Allocator) !void {
|
||||
};
|
||||
},
|
||||
.fetch => |opts| {
|
||||
log.debug(.app, "startup", .{ .mode = "fetch", .dump = opts.dump, .url = opts.url });
|
||||
log.debug(.app, "startup", .{ .mode = "fetch", .dump = opts.dump, .markdown = opts.markdown, .url = opts.url });
|
||||
const url = try @import("url.zig").URL.parse(opts.url, null);
|
||||
|
||||
// browser
|
||||
@@ -128,6 +128,13 @@ 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());
|
||||
@@ -193,6 +200,7 @@ const Command = struct {
|
||||
const Fetch = struct {
|
||||
url: []const u8,
|
||||
dump: bool = false,
|
||||
markdown: bool = false,
|
||||
common: Common,
|
||||
};
|
||||
|
||||
@@ -241,6 +249,9 @@ 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
|
||||
@@ -317,6 +328,9 @@ 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;
|
||||
}
|
||||
@@ -402,6 +416,7 @@ 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 = .{};
|
||||
|
||||
@@ -410,6 +425,10 @@ fn parseFetchArgs(
|
||||
dump = true;
|
||||
continue;
|
||||
}
|
||||
if (std.mem.eql(u8, "--markdown", opt)) {
|
||||
markdown = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (try parseCommonArg(allocator, opt, args, &common)) {
|
||||
continue;
|
||||
@@ -435,6 +454,7 @@ fn parseFetchArgs(
|
||||
return .{
|
||||
.url = url.?,
|
||||
.dump = dump,
|
||||
.markdown = markdown,
|
||||
.common = common,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -337,6 +337,7 @@ 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;
|
||||
@@ -351,7 +352,6 @@ 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/Scope that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
|
||||
// The main Context 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,6 +463,38 @@ 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;
|
||||
}
|
||||
@@ -1158,7 +1190,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
}
|
||||
|
||||
if (!js_value.isArray()) {
|
||||
return error.InvalidArgument;
|
||||
return .{ .invalid = {} };
|
||||
}
|
||||
|
||||
// This can get tricky.
|
||||
@@ -1227,12 +1259,25 @@ 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 error", .{ .specifier = specifier, .err = err });
|
||||
log.err(.js, "resolve module fetch", .{
|
||||
.err = err,
|
||||
.specifier = specifier,
|
||||
});
|
||||
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 error", .{ .specifier = specifier, .err = 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),
|
||||
});
|
||||
return null;
|
||||
};
|
||||
return m.handle;
|
||||
@@ -1257,6 +1302,16 @@ 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
|
||||
@@ -1271,6 +1326,33 @@ 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);
|
||||
}
|
||||
@@ -1450,6 +1532,11 @@ 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
|
||||
@@ -1472,6 +1559,10 @@ 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 {
|
||||
@@ -1501,6 +1592,20 @@ 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
|
||||
@@ -1736,7 +1841,9 @@ 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 => generateAttribute(Struct, name, isolate, template, template_proto),
|
||||
else => |ti| if (!comptime isComplexAttributeType(ti)) {
|
||||
generateAttribute(Struct, name, isolate, template, template_proto);
|
||||
},
|
||||
}
|
||||
} else if (comptime std.mem.startsWith(u8, name, "get_")) {
|
||||
generateProperty(Struct, name[4..], isolate, template_proto);
|
||||
@@ -1784,7 +1891,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;
|
||||
}
|
||||
@@ -1876,7 +1983,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
|
||||
defer caller.deinit();
|
||||
|
||||
const named_function = comptime NamedFunction.init(Struct, "get_" ++ name);
|
||||
caller.getter(Struct, named_function, info) catch |err| {
|
||||
caller.method(Struct, named_function, info) catch |err| {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
};
|
||||
}
|
||||
@@ -1891,13 +1998,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.setter(Struct, named_function, js_value, info) catch |err| {
|
||||
caller.method(Struct, named_function, info) catch |err| {
|
||||
caller.handleError(Struct, named_function, err, info);
|
||||
};
|
||||
}
|
||||
@@ -2323,6 +2430,20 @@ 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
|
||||
@@ -2424,66 +2545,6 @@ 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);
|
||||
@@ -2596,19 +2657,14 @@ fn Caller(comptime E: type, comptime State: type) type {
|
||||
|
||||
if (comptime builtin.mode == .Debug and @hasDecl(@TypeOf(info), "length")) {
|
||||
if (log.enabled(.js, .warn)) {
|
||||
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),
|
||||
});
|
||||
logFunctionCallError(self.call_arena, self.isolate, self.v8_context, err, named_function.full_name, info);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -2670,6 +2726,7 @@ 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;
|
||||
@@ -2721,8 +2778,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
|
||||
@@ -2808,28 +2865,6 @@ 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3270,6 +3305,37 @@ 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.
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// some utils to parser strings.
|
||||
const std = @import("std");
|
||||
|
||||
pub const Reader = struct {
|
||||
pos: usize = 0,
|
||||
data: []const u8,
|
||||
|
||||
pub fn until(self: *Reader, c: u8) []const u8 {
|
||||
const pos = self.pos;
|
||||
const data = self.data;
|
||||
|
||||
const index = std.mem.indexOfScalarPos(u8, data, pos, c) orelse data.len;
|
||||
self.pos = index;
|
||||
return data[pos..index];
|
||||
}
|
||||
|
||||
pub fn tail(self: *Reader) []const u8 {
|
||||
const pos = self.pos;
|
||||
const data = self.data;
|
||||
if (pos > data.len) {
|
||||
return "";
|
||||
}
|
||||
self.pos = data.len;
|
||||
return data[pos..];
|
||||
}
|
||||
|
||||
pub fn skip(self: *Reader) bool {
|
||||
const pos = self.pos;
|
||||
if (pos >= self.data.len) {
|
||||
return false;
|
||||
}
|
||||
self.pos = pos + 1;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// converts a comptime-known string (i.e. null terminated) to an uint
|
||||
pub fn asUint(comptime string: anytype) AsUintReturn(string) {
|
||||
const byteLength = @bitSizeOf(@TypeOf(string.*)) / 8 - 1;
|
||||
const expectedType = *const [byteLength:0]u8;
|
||||
if (@TypeOf(string) != expectedType) {
|
||||
@compileError("expected : " ++ @typeName(expectedType) ++
|
||||
", got: " ++ @typeName(@TypeOf(string)));
|
||||
}
|
||||
|
||||
return @bitCast(@as(*const [byteLength]u8, string).*);
|
||||
}
|
||||
|
||||
fn AsUintReturn(comptime string: anytype) type {
|
||||
return @Type(.{
|
||||
.int = .{
|
||||
.bits = @bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0
|
||||
.signedness = .unsigned,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const testing = std.testing;
|
||||
test "parser.Reader: skip" {
|
||||
var r = Reader{ .data = "foo" };
|
||||
try testing.expectEqual(true, r.skip());
|
||||
try testing.expectEqual(true, r.skip());
|
||||
try testing.expectEqual(true, r.skip());
|
||||
try testing.expectEqual(false, r.skip());
|
||||
try testing.expectEqual(false, r.skip());
|
||||
}
|
||||
|
||||
test "parser.Reader: tail" {
|
||||
var r = Reader{ .data = "foo" };
|
||||
try testing.expectEqualStrings("foo", r.tail());
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
}
|
||||
|
||||
test "parser.Reader: until" {
|
||||
var r = Reader{ .data = "foo.bar.baz" };
|
||||
try testing.expectEqualStrings("foo", r.until('.'));
|
||||
_ = r.skip();
|
||||
try testing.expectEqualStrings("bar", r.until('.'));
|
||||
_ = r.skip();
|
||||
try testing.expectEqualStrings("baz", r.until('.'));
|
||||
|
||||
r = Reader{ .data = "foo" };
|
||||
try testing.expectEqualStrings("foo", r.until('.'));
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
|
||||
r = Reader{ .data = "" };
|
||||
try testing.expectEqualStrings("", r.until('.'));
|
||||
try testing.expectEqualStrings("", r.tail());
|
||||
}
|
||||
|
||||
test "parser: asUint" {
|
||||
const ASCII_x = @as(u8, @bitCast([1]u8{'x'}));
|
||||
const ASCII_ab = @as(u16, @bitCast([2]u8{ 'a', 'b' }));
|
||||
const ASCII_xyz = @as(u24, @bitCast([3]u8{ 'x', 'y', 'z' }));
|
||||
const ASCII_abcd = @as(u32, @bitCast([4]u8{ 'a', 'b', 'c', 'd' }));
|
||||
|
||||
try testing.expectEqual(ASCII_x, asUint("x"));
|
||||
try testing.expectEqual(ASCII_ab, asUint("ab"));
|
||||
try testing.expectEqual(ASCII_xyz, asUint("xyz"));
|
||||
try testing.expectEqual(ASCII_abcd, asUint("abcd"));
|
||||
|
||||
try testing.expectEqual(u8, @TypeOf(asUint("x")));
|
||||
try testing.expectEqual(u16, @TypeOf(asUint("ab")));
|
||||
try testing.expectEqual(u24, @TypeOf(asUint("xyz")));
|
||||
try testing.expectEqual(u32, @TypeOf(asUint("abcd")));
|
||||
}
|
||||
Reference in New Issue
Block a user