54 Commits

Author SHA1 Message Date
Francis Bouvier
3575f45ac0 markdown: working duckduckgo 2025-06-19 11:22:05 -07:00
Francis Bouvier
326851ed6f markdown: first implementation 2025-06-16 16:06:59 -07:00
Karl Seguin
5dcc3db36b Merge pull request #795 from lightpanda-io/performance_observer
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Add dummy PerformanceObserver
2025-06-20 08:01:09 +08:00
Karl Seguin
c5d49a9d34 Add dummy PerformanceObserver
Adds a dummy PerformanceObserver. Only the supportedEntryTypes static attribute
is supported, and it currently returns an empty array. This hopefully prevents
code from trying to use it. For example, before using it, reddit checks if
specific types are supported and, if not, doesn't use it.

This introduced complexity in the js runtime. Our current approach to
attributes only works with primitive types. Non-primitive types can't be
attached to a FunctionTemplate (v8 will crash saying only primitive types can
be set). Plus, all non primitive types require a context to create anyways.

We now detect "primitive" attributes and "complex" attributes. Primitive
attributes are setup as before. Complex attributes are setup per-context,
requiring another loop through our types to detect & setup on each context
creation.
2025-06-19 18:20:02 +08:00
Karl Seguin
ef9f828d35 Merge pull request #790 from lightpanda-io/css-stylesheet
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
Minimal CSSStyleSheet
2025-06-19 10:32:13 +08:00
Karl Seguin
c691764205 Merge pull request #794 from lightpanda-io/window-screen
add Screen and ScreenOrientation
2025-06-19 10:29:52 +08:00
sjorsdonkers
2c940d4fd6 browser context proxyServer 2025-06-19 10:26:33 +08:00
Karl Seguin
54bd55d45d fix CSSStyleSheet prototype 2025-06-19 10:25:13 +08:00
Karl Seguin
0b846b15b1 Merge pull request #789 from lightpanda-io/browsercontext-proxyServer
browser context proxyServer
2025-06-19 10:22:17 +08:00
Muki Kiboigo
269eb7e154 add Screen and ScreenOrientation 2025-06-18 12:53:54 -07:00
Muki Kiboigo
97bc19e4ae clean up various imports in CSSOM 2025-06-18 11:32:28 -07:00
Muki Kiboigo
2656cc7842 Add basic tests for CSSStyleSheet 2025-06-18 11:32:28 -07:00
Muki Kiboigo
ba94818415 add CSSStyleSheet 2025-06-18 11:32:27 -07:00
Pierre Tachoire
ac759a6eed Merge pull request #793 from lightpanda-io/domrect-bottom
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
add top, left, bottom, right to DOMRect
2025-06-18 09:54:08 -07:00
Pierre Tachoire
1839b346a6 Merge pull request #792 from lightpanda-io/fix_current_script_scope
Fixes the scoping of page.current_script
2025-06-18 09:51:41 -07:00
Pierre Tachoire
c1ffe7f8e6 Merge pull request #791 from lightpanda-io/zig_event_target_fix
Fix crash when event.currentTarget is used with EventTargetTBase
2025-06-18 08:26:25 -07:00
Pierre Tachoire
833b4d10bd add top, left, bottom, right to DOMRect 2025-06-18 08:21:33 -07:00
Pierre Tachoire
ce98c336c9 keep EventTargetTBase as the dom_event_target
Mimic a dom_node by adding the refcnt field right after the vtable
pointer.
2025-06-18 07:08:35 -07:00
Karl Seguin
d05619990a Fixes the scoping of page.current_script
This was previously being set back to null before it was actually needed.

Also, added a more logs / log details.
2025-06-18 18:36:00 +08:00
Karl Seguin
8033e41d4a Merge pull request #788 from lightpanda-io/dont_keepalive_unprocess_request
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Delay setting the requests' keepalive flag until the request is fully…
2025-06-18 16:46:45 +08:00
sjorsdonkers
60f4eab759 handle no params 2025-06-18 10:07:37 +02:00
sjorsdonkers
d7656ea985 expires dashes and f64 2025-06-18 10:07:37 +02:00
sjorsdonkers
e402998577 JS may not set/get HttpOnly cookies 2025-06-18 10:07:37 +02:00
sjorsdonkers
073f75efa3 CDP Network cookie tests 2025-06-18 10:07:37 +02:00
sjorsdonkers
da414f7eb3 CDP.Storage cookies tests 2025-06-18 10:07:37 +02:00
sjorsdonkers
270b89830a Cleaning up crumbles 2025-06-18 10:07:37 +02:00
sjorsdonkers
74ce7ca416 refactor path / domain parsing 2025-06-18 10:07:37 +02:00
sjorsdonkers
3f4338cb51 wip 2025-06-18 10:07:37 +02:00
sjorsdonkers
30ee41fd0e Network.getCookies 2025-06-18 10:07:37 +02:00
sjorsdonkers
4965fec55c storage cookies 2025-06-18 10:07:37 +02:00
sjorsdonkers
18dff8455c lower case domain 2025-06-18 10:07:37 +02:00
sjorsdonkers
fe16f06aee clearRetainingCapacity 2025-06-18 10:07:37 +02:00
sjorsdonkers
48c1c05a93 setCookie 2025-06-18 10:07:37 +02:00
sjorsdonkers
38dee1166d setCookies 2025-06-18 10:07:37 +02:00
sjorsdonkers
0c6fc68eae deleteCookies 2025-06-18 10:07:37 +02:00
Karl Seguin
223611d89e Fix crash when event.currentTarget is used with EventTargetTBase
When EventTargetTBase is used, we pass the container as the target to libdom.
This is not safe, as libdom is expecting an event_target. We see, for example
that when _dom_event_get_current_target is called, the refcnt is increased.
This works if the current_target is a valid event_target, but if it's a
Zig instance (like the Window) ... we're just altering some bits of the
window instance.

This attempts to add a dummy target to EventTargetTBase which can acts as a
real event_targt in place of the Zig instance.
2025-06-18 14:49:15 +08:00
sjorsdonkers
6f5141d5fb browser context proxyServer 2025-06-17 18:43:12 +02:00
Karl Seguin
a6ac7d9c4e Delay setting the requests' keepalive flag until the request is fully processed
We currently set request._keepalive prematurely. There are [error cases] where
the request could be abandoned before being fully drained. While we do try to
drain in some cases, it isn't always possible. For this reason,
request.keepalive is only set at the end of the request lifecycle, at which
point we know the connection is ready to be re-used.
2025-06-17 19:55:36 +08:00
Karl Seguin
9b35736be3 Merge pull request #786 from lightpanda-io/custom-elements
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
add CustomElementRegistry
2025-06-17 15:53:34 +08:00
Karl Seguin
9f54cb35f4 remove unused import and debug stmt 2025-06-17 08:20:11 +08:00
Karl Seguin
329bffb127 Fix non-probing of union array failure
Probing a union match for an possible value should rarely hard-fail. Instead,
it should return an .{.invalid = {}} response to let the prober decide how to
proceed. This fixes a hard-fail when a JS value fails to probe as an array.

Also, add :modal pseudo-class.

(both issues came up looking at github integration)
2025-06-17 08:19:01 +08:00
Karl Seguin
e2542f41b5 Improve build and test speed
Test speed has been improved only slightly by tweaking a 2-second running tests.

Build has been improved by:
1 - moving logFunctionCallError out of js.Caller and to a standalone function
2 - removing some non-generic code from the generic portions of the logger

Caller.getter and Caller.setter have been removed in favor or calling
Caller.method. This wasn't previously possible - prior to our v8 upgrade, they
had different signatures.

Also removed a largely unused parser/str.zig file.
2025-06-17 08:19:01 +08:00
Karl Seguin
efc7b9d4a5 Add comment explaining why we're walking the form the way we are 2025-06-17 08:19:01 +08:00
Karl Seguin
72915760c4 Use css.querySelectorAll to find form elements
Libdom's formGetCollection doesn't work (like I would expect) for dynamically
added elements.

For example, given:

```
let el = document.createElement('input');
document.getElementsByTagName('form')[0].append(el);
```

(and assume the page has a form), I'd expect `el.form` to be equal to the form
the input was added to. Instead, it's null. This is a problem given that
`dom_html_form_element_get_elements` uses the element's `form` attribute to
"collect" the elements.

This uses our existing querySelector to find the form elements.
2025-06-17 08:19:01 +08:00
Karl Seguin
e9d7a946c5 Merge pull request #784 from lightpanda-io/union_param_array_fix
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Fix non-probing of union array failure
2025-06-17 08:08:04 +08:00
Karl Seguin
714e5e0456 Merge pull request #785 from lightpanda-io/build_and_test_speed
Improve build and test speed
2025-06-17 08:07:43 +08:00
Karl Seguin
26e8642aca Merge pull request #782 from lightpanda-io/form_support_dynamic_elements
Use css.querySelectorAll to find form elements
2025-06-17 08:07:30 +08:00
Karl Seguin
68dfb4ee86 fix custom elements when minified js is used 2025-06-16 07:47:53 -07:00
Karl Seguin
f1ff789334 implement custom elements - i think/hope 2025-06-16 07:45:49 -07:00
Muki Kiboigo
1f45d5b8e4 add CustomElementRegistry 2025-06-16 07:35:55 -07:00
Karl Seguin
c20052f314 Add comment explaining why we're walking the form the way we are 2025-06-16 19:56:19 +08:00
Karl Seguin
c28d87d59c Improve build and test speed
Test speed has been improved only slightly by tweaking a 2-second running tests.

Build has been improved by:
1 - moving logFunctionCallError out of js.Caller and to a standalone function
2 - removing some non-generic code from the generic portions of the logger

Caller.getter and Caller.setter have been removed in favor or calling
Caller.method. This wasn't previously possible - prior to our v8 upgrade, they
had different signatures.

Also removed a largely unused parser/str.zig file.
2025-06-16 19:50:13 +08:00
Karl Seguin
237ddcba9a Fix non-probing of union array failure
Probing a union match for an possible value should rarely hard-fail. Instead,
it should return an .{.invalid = {}} response to let the prober decide how to
proceed. This fixes a hard-fail when a JS value fails to probe as an array.

Also, add :modal pseudo-class.

(both issues came up looking at github integration)
2025-06-16 17:07:43 +08:00
Karl Seguin
eadb5b6461 Use css.querySelectorAll to find form elements
Libdom's formGetCollection doesn't work (like I would expect) for dynamically
added elements.

For example, given:

```
let el = document.createElement('input');
document.getElementsByTagName('form')[0].append(el);
```

(and assume the page has a form), I'd expect `el.form` to be equal to the form
the input was added to. Instead, it's null. This is a problem given that
`dom_html_form_element_get_elements` uses the element's `form` attribute to
"collect" the elements.

This uses our existing querySelector to find the form elements.
2025-06-16 13:49:34 +08:00
37 changed files with 2374 additions and 547 deletions

View File

@@ -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 },
}
}

View File

@@ -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;
}
};

View 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,
};

View 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" },
}, .{});
}

View File

@@ -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;
}

View 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" },
}, .{});
}

View 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,
};

View 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;
}
};

View File

@@ -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 {

View File

@@ -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,
};

View File

@@ -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);
}

View 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" },
}, .{});
}

View File

@@ -22,7 +22,7 @@ const WebApis = struct {
pub const Interfaces = generate.Tuple(.{
@import("crypto/crypto.zig").Crypto,
@import("console/console.zig").Console,
@import("cssom/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,
});
};

View File

@@ -81,7 +81,7 @@ pub const HTMLDocument = struct {
pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 {
var buf: std.ArrayListUnmanaged(u8) = .{};
try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true });
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(&.{

View File

@@ -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, .{});

View File

@@ -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
View 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" },
}, .{});
}

View File

@@ -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
View 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, "![", writer);
if (try getAttributeValue(element, "alt")) |alt| {
try writeInline(state, alt, writer);
} else {
try writeInline(state, src, writer);
}
try writeInline(state, "](", writer);
// handle relative path
if (src[0] == '/') {
try writeInline(state, url.scheme(), writer);
try writeInline(state, "://", writer);
try writeInline(state, url.host(), writer);
}
try writeInline(state, src, writer);
try writeInline(state, ")", writer);
}
},
.ul => {
var elem = Elem{ .parent = state.elem, .list_order = 0 };
state.elem = &elem;
defer state.elem = elem.parent;
try ensureBlock(state, writer);
try writeChildren(url, node, state, writer);
try ensureBlock(state, writer);
},
.ol => {
var elem = Elem{ .parent = state.elem, .list_order = 1 };
state.elem = &elem;
defer state.elem = elem.parent;
try ensureBlock(state, writer);
try writeChildren(url, node, state, writer);
try ensureBlock(state, writer);
},
.li => blk: {
const parent = state.elem orelse break :blk;
const list_order = parent.list_order orelse break :blk;
if (!state.block) try writer.writeAll("\n");
if (list_order > 0) {
// ordered list
try writeInline(state, order[list_order - 1], writer);
try writeInline(state, ". ", writer);
parent.list_order = list_order + 1;
} else {
// unordered list
try writeInline(state, "- ", writer);
}
try writeChildren(url, node, state, writer);
},
.input => {
var elem = Elem{ .parent = state.elem, .inlin = true };
state.elem = &elem;
defer state.elem = elem.parent;
const element = parser.nodeToElement(node);
if (try getAttributeValue(element, "value")) |value| {
try writeInline(state, value, writer);
try writeInline(state, " ", writer);
}
},
else => {
try ensureBlock(state, writer);
try writer.writeAll(@tagName(tag));
try writer.writeAll(" not supported");
try ensureBlock(state, writer);
},
}
// try writer.writeAll("\nend - ");
// try writer.writeAll(@tagName(tag));
// try writer.writeAll("\n");
},
.text => {
const v = try parser.nodeValue(node) orelse return;
const printed = try writeText(state, v, writer);
if (printed) state.block = false;
},
.cdata_section => {},
.comment => {},
// TODO handle processing instruction dump
.processing_instruction => {},
// document fragment is outside of the main document DOM, so we
// don't output it.
.document_fragment => {},
// document will never be called, but required for completeness.
.document => {},
// done globally instead, but required for completeness. Only the outer DOCTYPE should be written
.document_type => {},
// deprecated
.attribute, .entity_reference, .entity, .notation => {},
}
}
// TODO: not sure about + - . ! as they are very common characters
// I fear that we add too much escape strings
// TODO: | (pipe)
const escape = [_]u8{ '\\', '`', '*', '_', '{', '}', '[', ']', '<', '>', '(', ')', '#' };
fn writeText(state: *State, value: []const u8, writer: anytype) !bool {
if (value.len == 0) return false;
var last_char: u8 = ' ';
var printed: u64 = 0;
for (value, 0..) |v, i| {
// do not print:
// - multiple spaces
// - return line
// - tabs
if (v == last_char and v == ' ') continue;
if (v == '\n') continue;
if (v == '\t') continue;
// escape char
for (escape) |esc| {
if (v == esc) try writer.writeAll("\\");
}
if (printed == 0 and !state.is_inline()) {
if (state.last_char != '\n' and state.last_char != ' ') {
try writer.writeAll(" ");
}
}
last_char = v;
printed += 1;
const x = [_]u8{v}; // TODO: do we have something better?
try writer.writeAll(&x);
if (i == value.len - 1) state.last_char = v;
}
if (printed > 0) return true;
return false;
}
fn getAttributeValue(elem: *parser.Element, attr: []const u8) !?[]const u8 {
if (try parser.elementGetAttribute(elem, attr)) |value| {
if (value.len > 0) return value;
}
return null;
}
fn writeEscapedTextNode(writer: anytype, value: []const u8) !void {
var v = value;
while (v.len > 0) {
try writer.writeAll("TEXT: ");
const index = std.mem.indexOfAnyPos(u8, v, 0, &.{ '&', '<', '>' }) orelse {
return writer.writeAll(v);
};
try writer.writeAll(v[0..index]);
switch (v[index]) {
'&' => try writer.writeAll("&amp;"),
'<' => try writer.writeAll("&lt;"),
'>' => try writer.writeAll("&gt;"),
else => unreachable,
}
v = v[index + 1 ..];
}
}

View File

@@ -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 {

View File

@@ -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: {

View File

@@ -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,
};
}

View File

@@ -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 "/";
};
return try allocator.dupe(u8, document_path[0 .. last + 1]);
}
pub const PreparedUri = struct {
host: []const u8, // Percent encoded, lower case
path: []const u8, // Percent encoded
secure: bool, // True if scheme is https
};
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));
}

View 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" },
}, .{});
}

View 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,
};

View File

@@ -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
},
}, .{});
}

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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
View 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",
};

View File

@@ -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,
}, .{});

View File

@@ -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);

View File

@@ -146,6 +146,16 @@ fn logTo(comptime scope: Scope, level: Level, comptime msg: []const u8, data: an
}
fn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
try logLogFmtPrefix(scope, level, msg, writer);
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
const key = " " ++ f.name ++ "=";
try writer.writeAll(key);
try writeValue(.logfmt, @field(data, f.name), writer);
}
try writer.writeByte('\n');
}
fn logLogFmtPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: anytype) !void {
try writer.writeAll("$time=");
try writer.print("{d}", .{timestamp()});
@@ -164,15 +174,20 @@ fn logLogfmt(comptime scope: Scope, level: Level, comptime msg: []const u8, data
break :blk prefix ++ "\"" ++ msg ++ "\"";
};
try writer.writeAll(full_msg);
}
fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
try logPrettyPrefix(scope, level, msg, writer);
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
const key = " " ++ f.name ++ "=";
const key = " " ++ f.name ++ " = ";
try writer.writeAll(key);
try writeValue(.logfmt, @field(data, f.name), writer);
try writeValue(.pretty, @field(data, f.name), writer);
try writer.writeByte('\n');
}
try writer.writeByte('\n');
}
fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data: anytype, writer: anytype) !void {
fn logPrettyPrefix(comptime scope: Scope, level: Level, comptime msg: []const u8, writer: anytype) !void {
if (scope == .console and level == .fatal and comptime std.mem.eql(u8, msg, "lightpanda")) {
try writer.writeAll("\x1b[0;104mWARN ");
} else {
@@ -201,14 +216,6 @@ fn logPretty(comptime scope: Scope, level: Level, comptime msg: []const u8, data
try writer.print(" \x1b[0m[+{d}ms]", .{elapsed()});
try writer.writeByte('\n');
}
inline for (@typeInfo(@TypeOf(data)).@"struct".fields) |f| {
const key = " " ++ f.name ++ " = ";
try writer.writeAll(key);
try writeValue(.pretty, @field(data, f.name), writer);
try writer.writeByte('\n');
}
try writer.writeByte('\n');
}
pub fn writeValue(comptime format: Format, value: anytype, writer: anytype) !void {

View File

@@ -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,
};
}

View File

@@ -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;
}
@@ -1857,7 +1964,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
// apply it both to the type itself
template.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
// andto instances of the type
// and to instances of the type
template_proto.set(js_name, js_value, v8.PropertyAttribute.ReadOnly + v8.PropertyAttribute.DontDelete);
}
@@ -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.

View File

@@ -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")));
}