76 Commits

Author SHA1 Message Date
Pierre Tachoire
02cd5e98f9 event_target: handle window target 2025-01-21 13:50:34 +01:00
Pierre Tachoire
a51c20068f netsurf: handle event target get type 2025-01-21 13:50:34 +01:00
Pierre Tachoire
99a3e4be3f upgrade vendor/netsurf/libdom 2025-01-21 13:50:33 +01:00
Pierre Tachoire
40c9355088 Merge pull request #355 from lightpanda-io/history
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
dom: history placeholder
2025-01-15 11:55:50 +01:00
Pierre Tachoire
8f1557254a typo fix 2025-01-15 11:45:30 +01:00
Pierre Tachoire
11d28b0bc3 dom: add placeholder for history interface 2025-01-15 11:45:30 +01:00
Pierre Tachoire
974cf780c0 dom: clean history file 2025-01-14 15:04:40 +01:00
Pierre Tachoire
73bb14e4a9 Merge pull request #285 from lightpanda-io/cdp-cdpcli
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
cdp: cdpcli compatibility
2025-01-13 18:16:40 +01:00
Pierre Tachoire
daf4236023 runtime: fix sessionid 2025-01-13 18:08:09 +01:00
Pierre Tachoire
4c9a24c64e start inspector when the js env starts 2025-01-13 10:53:38 +01:00
Pierre Tachoire
c149f65158 cdp: remove event dispateched by inspector 2025-01-13 10:53:36 +01:00
Pierre Tachoire
c5688c1bd3 cdp: display last message on cdp error 2025-01-13 10:53:35 +01:00
Pierre Tachoire
b276a15786 cdp: add target.detachFromTarget noop 2025-01-13 10:53:33 +01:00
Pierre Tachoire
2fed239ece browser: split page start from page navigate 2025-01-13 10:53:29 +01:00
Pierre Tachoire
8e2cb36597 cdp: fix some id inconsitency accross runtime messages 2025-01-13 10:49:48 +01:00
Pierre Tachoire
bcaace1c91 cdp: use identifiable hard coded ids 2025-01-13 10:47:51 +01:00
Pierre Tachoire
d664d07141 cdp: dispatch executionContextCreated on Runtime.enable 2025-01-13 10:47:42 +01:00
Pierre Tachoire
cb8b80c856 Merge pull request #345 from lightpanda-io/modules
browser: support for modules
2025-01-13 10:45:31 +01:00
Pierre Tachoire
d777d77b06 ci: update sig-v8 version 2025-01-13 10:36:52 +01:00
Pierre Tachoire
43678f8dc0 upgrade zig-js-runtime 2025-01-13 10:36:34 +01:00
Pierre Tachoire
5811577824 Merge pull request #354 from lightpanda-io/navigator-fix
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
zig-test / demo-puppeteer (push) Blocked by required conditions
navigator: remove useless import
2025-01-13 09:37:14 +01:00
Pierre Tachoire
1587122efa navigator: remove useless import 2025-01-10 17:42:20 +01:00
Pierre Tachoire
48e7c8ad0f browser: implement fetch module 2025-01-10 16:48:45 +01:00
Pierre Tachoire
766f9798f6 browser: load module 2025-01-09 11:48:39 +01:00
Pierre Tachoire
680d634725 update zig-js-runtime 2025-01-09 11:48:39 +01:00
Pierre Tachoire
7ac945bf88 browser: refacto script 2025-01-09 11:48:34 +01:00
Pierre Tachoire
188d7a8558 Merge pull request #352 from utay/fix-shell-main
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
Fix shell main
2025-01-08 17:15:18 +01:00
Pierre Tachoire
ee561b0d1e Merge pull request #353 from lightpanda-io/cdp-enable-security
Cdp enable security
2025-01-08 16:55:31 +01:00
Pierre Tachoire
82bbe78e95 cdp: return correct result on Runtime 2025-01-08 16:46:05 +01:00
Pierre Tachoire
c761cd059b cdp: log errors on sendMessageToTarget 2025-01-08 16:17:29 +01:00
Pierre Tachoire
03e87155ca cdp: add security.enable 2025-01-08 16:17:20 +01:00
Yannick Utard
ea39cc52b1 Fix shell main 2025-01-08 15:28:19 +01:00
Pierre Tachoire
90fb90b186 Merge pull request #351 from lightpanda-io/ignore-blank
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
zig-test / demo-puppeteer (push) Blocked by required conditions
browser: ignore about:blank navigation
2025-01-08 13:54:00 +01:00
Pierre Tachoire
5f8327eaf7 browser: ignore about:blank navigation 2025-01-08 13:44:41 +01:00
Pierre Tachoire
07869f3c48 Merge pull request #350 from lightpanda-io/cdp-enable
Cdp enable
2025-01-08 13:42:42 +01:00
Pierre Tachoire
8377eb02a5 cdp: add CSS.enable 2025-01-08 12:01:18 +01:00
Pierre Tachoire
3738e8eb44 cdp: add DOM.enable 2025-01-08 12:01:18 +01:00
Pierre Tachoire
4b000e44b3 cdp: add Inspector.enable 2025-01-08 12:01:18 +01:00
Pierre Tachoire
d6021d1702 Merge pull request #348 from lightpanda-io/dom-navigator
Dom navigator
2025-01-08 11:47:44 +01:00
Pierre Tachoire
b391eafc38 wpt: update tests 2025-01-08 11:38:11 +01:00
Pierre Tachoire
2af4545e10 dom: implement more navigator API 2025-01-08 11:03:26 +01:00
Pierre Tachoire
b96644d893 dom: implement navigatorLanguage 2025-01-08 11:03:26 +01:00
Pierre Tachoire
3cb77c0a32 dom: implement navigatorID 2025-01-08 11:03:25 +01:00
Pierre Tachoire
b3f7fb7be3 dom: implement navigator.userAgent 2025-01-08 11:03:11 +01:00
Pierre Tachoire
9fb51a1f29 Merge pull request #346 from lightpanda-io/target-created
cdp: add TargetCreated event on createTarget message
2025-01-08 10:10:18 +01:00
Pierre Tachoire
d78e8a725d cdp: remove useless parameter 2025-01-08 09:57:28 +01:00
Pierre Tachoire
7829bdbc95 Merge pull request #347 from lightpanda-io/send-message-to-target
cdp: add Target.sendMessageToTarget support
2025-01-07 16:21:41 +01:00
Pierre Tachoire
90ba6deba2 cdp: add Target.sendMessageToTarget support
see https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-sendMessageToTarget
2025-01-07 16:00:44 +01:00
Pierre Tachoire
5fc763a738 cdp: add TargetCreated event on createTarget message 2025-01-07 10:34:47 +01:00
Pierre Tachoire
84614e903c Merge pull request #344 from lightpanda-io/test-e2e
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
zig-test / demo-puppeteer (push) Has been cancelled
Add end to end tests
2025-01-06 10:13:51 +01:00
Pierre Tachoire
c95d739347 ci: add test end to end
using puppeteer test from https://github.com/lightpanda-io/demo
2024-12-31 12:01:27 +01:00
Pierre Tachoire
c55849cef5 Merge pull request #343 from lightpanda-io/upgrade-zig-js
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
upgrade zig-js-runtime deps
2024-12-31 10:37:22 +01:00
Pierre Tachoire
88adb09417 upgrade zig-js-runtime deps 2024-12-31 10:09:48 +01:00
Pierre Tachoire
cb356ffca9 Merge pull request #341 from lightpanda-io/docker
fix docker port
2024-12-26 10:02:54 +01:00
Pierre Tachoire
2ba3f252ee fix docker port 2024-12-26 10:00:59 +01:00
Pierre Tachoire
ebe2c8e3dd Merge pull request #332 from lightpanda-io/verbose-option
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
add --verbose option
2024-12-17 09:41:58 +01:00
Francis Bouvier
ca2d63edcf Merge pull request #330 from lightpanda-io/contributing
add a CONTRIBUTING file
2024-12-16 15:54:17 +01:00
Francis Bouvier
489a1a1c37 Merge pull request #338 from lightpanda-io/README
README: be more precise on current status
2024-12-16 13:43:45 +01:00
Francis Bouvier
5cad13eea3 README: be more precise on current status
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-16 13:35:24 +01:00
Pierre Tachoire
a3e09e015c Merge pull request #329 from lightpanda-io/readme-getting-started
readme: add a quick start section
2024-12-16 09:34:10 +01:00
Pierre Tachoire
22a6accac1 readme: add a quick start section 2024-12-13 17:45:30 +01:00
Pierre Tachoire
277c97a959 Merge pull request #331 from lightpanda-io/continue-on-error
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
browser: don't stop processing page on error status code
2024-12-13 17:41:34 +01:00
Pierre Tachoire
c08bc3239a Merge pull request #334 from lightpanda-io/patch-1
Update README.md
2024-12-13 15:05:34 +01:00
katie-lpd
f798c7e0fb Update README.md 2024-12-13 14:45:02 +01:00
Pierre Tachoire
193780a88f cla: add katie-lpd to the allow list
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
2024-12-13 14:33:57 +01:00
Pierre Tachoire
ab4973ab6c add --verbose option 2024-12-13 12:29:05 +01:00
Pierre Tachoire
f9da815e8f browser: don't stop processing page on error status code 2024-12-13 11:56:18 +01:00
Pierre Tachoire
59e187d59a add a CONTRIBUTING file 2024-12-13 11:28:14 +01:00
Francis Bouvier
3b018e2a6d Merge pull request #325 from lightpanda-io/await_promise
Some checks failed
wpt / web platform tests (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig build release (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
cdp: runtime, replace `"awaitPromise":true` only if present
2024-12-09 11:19:45 +01:00
Francis Bouvier
d4939c8260 Merge pull request #322 from lightpanda-io/cdp_msg_nullable_params
Some checks are pending
wpt / web platform tests (push) Waiting to run
wpt / perf-fmt (push) Blocked by required conditions
zig-test / zig build dev (push) Waiting to run
zig-test / zig build release (push) Waiting to run
zig-test / zig test (push) Waiting to run
zig-test / perf-fmt (push) Blocked by required conditions
cdp: handle nullable Type for params
2024-12-08 19:08:27 +01:00
Francis Bouvier
485352594c Merge pull request #324 from lightpanda-io/json_parse_unknown_field
cdp: do not throw error on json parse for unknown fields
2024-12-08 19:05:11 +01:00
Francis Bouvier
bcdcfee467 Merge pull request #323 from lightpanda-io/cdp_msg_size
Cdp msg size
2024-12-08 18:58:14 +01:00
Francis Bouvier
0217e3fcae cdp: runtime, replace "awaitPromise":true only if present
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-08 15:33:32 +01:00
Francis Bouvier
6e7d6421d5 cdp: do not throw error on json parse for unknown fields
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-08 15:27:12 +01:00
Francis Bouvier
913d3af938 cdp: increase msg size 16KB -> 256KB
And move header size encoding from 2 bytes -> 2 bytes

Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-04 23:06:55 +01:00
Francis Bouvier
53dd0a5e4c cdp: handle nullable Type for params
Signed-off-by: Francis Bouvier <francis@lightpanda.io>
2024-12-04 22:39:54 +01:00
35 changed files with 1073 additions and 180 deletions

View File

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

View File

@@ -26,7 +26,7 @@ jobs:
path-to-document: 'https://github.com/lightpanda-io/browser/blob/main/CLA.md'
# branch should not be protected
branch: 'main'
allowlist: krichprollsch,francisbouvier
allowlist: krichprollsch,francisbouvier,katie-lpd
remote-organization-name: lightpanda-io
remote-repository-name: cla

View File

@@ -56,6 +56,14 @@ jobs:
- name: zig build debug
run: zig build -Dengine=v8
- name: upload artifact
uses: actions/upload-artifact@v4
with:
name: lightpanda-build-dev
path: |
zig-out/bin/lightpanda
retention-days: 1
zig-build-release:
name: zig build release
@@ -131,3 +139,30 @@ jobs:
- name: format and send json result
run: /perf-fmt bench-browser ${{ github.sha }} bench.json
demo-puppeteer:
name: demo-puppeteer
needs: zig-build-dev
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: 'lightpanda-io/demo'
fetch-depth: 0
- run: npm install
- name: download artifact
uses: actions/download-artifact@v4
with:
name: lightpanda-build-dev
- run: chmod a+x ./lightpanda
- name: run puppeteer
run: |
python3 -m http.server 1234 -d ./public &
./lightpanda &
RUNS=2 npm run bench-puppeteer-cdp

10
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,10 @@
# Contributing
Lightpanda accepts pull requests through GitHub.
You have to sign our [CLA](CLA.md) during your first pull request process
otherwise we're not able to accept your contributions.
The process signature uses the [CLA assistant
lite](https://github.com/marketplace/actions/cla-assistant-lite). You can see
an example of the process in [#303](https://github.com/lightpanda-io/browser/pull/303).

View File

@@ -74,4 +74,6 @@ COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.
COPY --from=0 /browser/zig-out/bin/lightpanda /bin/lightpanda
CMD ["/bin/lightpanda", "--host", "0.0.0.0", "--port", "3245"]
EXPOSE 9222/tcp
CMD ["/bin/lightpanda", "--host", "0.0.0.0", "--port", "9222"]

View File

@@ -13,13 +13,13 @@
Lightpanda is the open-source browser made for headless usage:
- Javascript execution
- Support of the Web APIs (partial, WIP)
- Support of Web APIs (partial, WIP)
- Compatible with Playwright, Puppeteer through CDP (WIP)
Fast scraping and web automation with minimal memory footprint:
- Ultra-low memory footprint (9x less than Chrome)
- Blazingly fast execution (11x faster than Chrome) & instant startup
- Exceptionally fast execution (11x faster than Chrome) & instant startup
<img width=500px src="https://cdn.lightpanda.io/assets/images/benchmark_2024-12-04.png">
@@ -29,14 +29,14 @@ See [benchmark details](https://github.com/lightpanda-io/demo).
### Javascript execution is mandatory for the modern web
Back in the good old times, grabbing a webpage was as easy as making an HTTP request, cURL-like. Its not possible anymore, because Javascript is everywhere, like it or not:
In the good old days, scraping a webpage was as easy as making an HTTP request, cURL-like. Its not possible anymore, because Javascript is everywhere, like it or not:
- Ajax, Single Page App, Infinite loading, “click to display”, instant search, etc.
- Ajax, Single Page App, infinite loading, “click to display”, instant search, etc.
- JS web frameworks: React, Vue, Angular & others
### Chrome is not the right tool
So if we need Javascript, why not use a real web browser. Lets take a huge desktop application, hack it, and run it on the server, right? Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure its such a good idea?
If we need Javascript, why not use a real web browser? Take a huge desktop application, hack it, and run it on the server. Hundreds or thousands of instances of Chrome if you use it at scale. Are you sure its such a good idea?
- Heavy on RAM and CPU, expensive to run
- Hard to package, deploy and maintain at scale
@@ -44,20 +44,22 @@ So if we need Javascript, why not use a real web browser. Lets take a huge de
### Lightpanda is built for performance
If we want both Javascript and performance, for a real headless browser, we need to start from scratch. Not yet another iteration of Chromium, really from a blank page. Crazy right? But thats we did:
If we want both Javascript and performance in a true headless browser, we need to start from scratch. Not another iteration of Chromium, really from a blank page. Crazy right? But thats we did:
- Not based on Chromium, Blink or WebKit
- Low-level system programming language (Zig) with optimisations in mind
- Opinionated, without graphical rendering
- Opinionated: without graphical rendering
## Status
Lightpanda is still a work in progress and is currently at a Beta stage.
:warning: You should expect most websites to fail or crash.
Here are the key features we have implemented:
- [x] HTTP loader
- [x] HTML parser and DOM tree
- [x] HTML parser and DOM tree (based on Netsurf libs)
- [x] Javascript support (v8)
- [x] Basic DOM APIs
- [x] Ajax
@@ -66,13 +68,77 @@ Here are the key features we have implemented:
- [x] DOM dump
- [x] Basic CDP/websockets server
NOTE: There are hundreds of Web APIs. Developing a browser, even just for headless mode, is a huge task. It's more about coverage than a _working/not working_ binary situation.
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
## Install
## Quick start
We do provide [nighly builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for Linux x86_64 and MacOS aarch64.
### Install from the nightly builds
You can download the last binary from the [nightly
builds](https://github.com/lightpanda-io/browser/releases/tag/nightly) for
Linux x86_64 and MacOS aarch64.
```console
# Download the binary
$ wget https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux
$ chmod a+x ./lightpanda-x86_64-linux
$ ./lightpanda-x86_64-linux -h
usage: ./lightpanda-x86_64-linux [options] [URL]
start Lightpanda browser
* if an url is provided the browser will fetch the page and exit
* otherwhise the browser starts a CDP server
-h, --help Print this help message and exit.
--host Host of the CDP server (default "127.0.0.1")
--port Port of the CDP server (default "9222")
--timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
--dump Dump document in stdout (fetch mode only)
```
### Dump an URL
```console
$ ./lightpanda-x86_64-linux --dump https://lightpanda.io
info(browser): GET https://lightpanda.io/ http.Status.ok
info(browser): fetch script https://api.website.lightpanda.io/js/script.js: http.Status.ok
info(browser): eval remote https://api.website.lightpanda.io/js/script.js: TypeError: Cannot read properties of undefined (reading 'pushState')
<!DOCTYPE html>
```
### Start a CDP server
```console
$ ./lightpanda-x86_64-linux --host 127.0.0.1 --port 9222
info(websocket): starting blocking worker to listen on 127.0.0.1:9222
info(server): accepting new conn...
```
Once the CDP server started, you can run a Puppeteer script by configuring the
`browserWSEndpoint`.
```js
'use scrict'
import puppeteer from 'puppeteer-core';
// use browserWSEndpoint to pass the Lightpanda's CDP server address.
const browser = await puppeteer.connect({
browserWSEndpoint: "ws://127.0.0.1:9222",
});
// The rest of your script remains the same.
const context = await browser.createBrowserContext();
const page = await context.newPage();
await page.goto('https://wikipedia.com/');
await page.close();
await context.close();
```
## Build from sources

View File

@@ -29,6 +29,7 @@ const Mime = @import("mime.zig");
const jsruntime = @import("jsruntime");
const Loop = jsruntime.Loop;
const Env = jsruntime.Env;
const Module = jsruntime.Module;
const apiweb = @import("../apiweb.zig");
@@ -46,12 +47,15 @@ const polyfill = @import("../polyfill/polyfill.zig");
const log = std.log.scoped(.browser);
pub const user_agent = "Lightpanda/1.0";
// Browser is an instance of the browser.
// You can create multiple browser instances.
// A browser contains only one session.
// TODO allow multiple sessions per browser.
pub const Browser = struct {
session: Session = undefined,
agent: []const u8 = user_agent,
const uri = "about:blank";
@@ -111,7 +115,7 @@ pub const Session = struct {
.uri = uri,
.alloc = alloc,
.arena = std.heap.ArenaAllocator.init(alloc),
.window = Window.create(null),
.window = Window.create(null, .{ .agent = user_agent }),
.loader = Loader.init(alloc),
.storageShed = storage.Shed.init(alloc),
.httpClient = undefined,
@@ -122,6 +126,21 @@ pub const Session = struct {
try self.env.load(&self.jstypes);
}
fn fetchModule(ctx: *anyopaque, referrer: ?jsruntime.Module, specifier: []const u8) !jsruntime.Module {
_ = referrer;
const self: *Session = @ptrCast(@alignCast(ctx));
if (self.page == null) return error.NoPage;
log.debug("fetch module: specifier: {s}", .{specifier});
const alloc = self.arena.allocator();
const body = try self.page.?.fetchData(alloc, specifier);
defer alloc.free(body);
return self.env.compileModule(body, specifier);
}
fn deinit(self: *Session) void {
if (self.page) |*p| p.end();
@@ -195,6 +214,31 @@ pub const Page = struct {
};
}
// start js env.
// - auxData: extra data forwarded to the Inspector
// see Inspector.contextCreated
pub fn start(self: *Page, auxData: ?[]const u8) !void {
// start JS env
log.debug("start js env", .{});
try self.session.env.start();
// register the module loader
try self.session.env.setModuleLoadFn(self.session, Session.fetchModule);
// add global objects
log.debug("setup global env", .{});
try self.session.env.bindGlobal(&self.session.window);
// load polyfills
try polyfill.load(self.arena.allocator(), self.session.env);
// inspector
if (self.session.inspector) |inspector| {
log.debug("inspector context created", .{});
inspector.contextCreated(self.session.env, "", self.origin orelse "://", auxData);
}
}
// reset js env and mem arena.
pub fn end(self: *Page) void {
self.session.env.stop();
@@ -254,6 +298,11 @@ pub const Page = struct {
log.debug("starting GET {s}", .{uri});
// if the uri is about:blank, nothing to do.
if (std.mem.eql(u8, "about:blank", uri)) {
return;
}
// own the url
if (self.rawuri) |prev| alloc.free(prev);
self.rawuri = try alloc.dupe(u8, uri);
@@ -276,18 +325,15 @@ pub const Page = struct {
const req = resp.req;
log.info("GET {any} {d}", .{ self.uri, req.response.status });
log.info("GET {any} {d}", .{ self.uri, @intFromEnum(req.response.status) });
// TODO handle redirection
if (req.response.status != .ok) {
log.debug("{?} {d} {s}", .{
req.response.version,
req.response.status,
req.response.reason,
// TODO log headers
});
return error.BadStatusCode;
}
log.debug("{?} {d} {s}", .{
req.response.version,
@intFromEnum(req.response.status),
req.response.reason,
// TODO log headers
});
// TODO handle charset
// https://html.spec.whatwg.org/#content-type
@@ -352,14 +398,6 @@ pub const Page = struct {
// https://html.spec.whatwg.org/#read-html
// start JS env
// TODO load the js env concurrently with the HTML parsing.
log.debug("start js env", .{});
try self.session.env.start();
// load polyfills
try polyfill.load(alloc, self.session.env);
// inspector
if (self.session.inspector) |inspector| {
inspector.contextCreated(self.session.env, "", self.origin.?, auxData);
@@ -371,10 +409,6 @@ pub const Page = struct {
.httpClient = &self.session.httpClient,
});
// add global objects
log.debug("setup global env", .{});
try self.session.env.bindGlobal(&self.session.window);
// browse the DOM tree to retrieve scripts
// TODO execute the synchronous scripts during the HTL parsing.
// TODO fetch the script resources concurrently but execute them in the
@@ -383,7 +417,7 @@ pub const Page = struct {
// sasync stores scripts which can be run asynchronously.
// for now they are just run after the non-async one in order to
// dispatch DOMContentLoaded the sooner as possible.
var sasync = std.ArrayList(*parser.Element).init(alloc);
var sasync = std.ArrayList(Script).init(alloc);
defer sasync.deinit();
const root = parser.documentToNode(doc);
@@ -398,21 +432,10 @@ pub const Page = struct {
}
const e = parser.nodeToElement(next.?);
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
// ignore non-script tags
if (tag != .script) continue;
// ignore non-js script.
// > type
// > Attribute is not set (default), an empty string, or a JavaScript MIME
// > type indicates that the script is a "classic script", containing
// > JavaScript code.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
const stype = try parser.elementGetAttribute(e, "type");
if (!isJS(stype)) {
continue;
}
const script = try Script.init(e) orelse continue;
if (script.kind == .unknown) continue;
// Ignore the defer attribute b/c we analyze all script
// after the document has been parsed.
@@ -426,8 +449,8 @@ pub const Page = struct {
// > then the classic script will be fetched in parallel to
// > parsing and evaluated as soon as it is available.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async
if (try parser.elementGetAttribute(e, "async") != null) {
try sasync.append(e);
if (script.isasync) {
try sasync.append(script);
continue;
}
@@ -450,7 +473,7 @@ pub const Page = struct {
// > page.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#notes
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
self.evalScript(script) catch |err| log.warn("evaljs: {any}", .{err});
try parser.documentHTMLSetCurrentScript(html_doc, null);
}
@@ -467,9 +490,9 @@ pub const Page = struct {
_ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, html_doc), evt);
// eval async scripts.
for (sasync.items) |e| {
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(e));
self.evalScript(e) catch |err| log.warn("evaljs: {any}", .{err});
for (sasync.items) |s| {
try parser.documentHTMLSetCurrentScript(html_doc, @ptrCast(s.element));
self.evalScript(s) catch |err| log.warn("evaljs: {any}", .{err});
try parser.documentHTMLSetCurrentScript(html_doc, null);
}
@@ -491,15 +514,15 @@ pub const Page = struct {
// evalScript evaluates the src in priority.
// if no src is present, we evaluate the text source.
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
fn evalScript(self: *Page, e: *parser.Element) !void {
fn evalScript(self: *Page, s: Script) !void {
const alloc = self.arena.allocator();
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
const opt_src = try parser.elementGetAttribute(e, "src");
const opt_src = try parser.elementGetAttribute(s.element, "src");
if (opt_src) |src| {
log.debug("starting GET {s}", .{src});
self.fetchScript(src) catch |err| {
self.fetchScript(s) catch |err| {
switch (err) {
FetchError.BadStatusCode => return err,
@@ -518,26 +541,10 @@ pub const Page = struct {
return;
}
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(self.session.env);
defer try_catch.deinit();
const opt_text = try parser.nodeTextContent(parser.elementToNode(e));
// TODO handle charset attribute
const opt_text = try parser.nodeTextContent(parser.elementToNode(s.element));
if (opt_text) |text| {
// TODO handle charset attribute
const res = self.session.env.exec(text, "") catch {
if (try try_catch.err(alloc, self.session.env)) |msg| {
defer alloc.free(msg);
log.info("eval inline {s}: {s}", .{ text, msg });
}
return;
};
if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, self.session.env);
defer alloc.free(msg);
log.debug("eval inline {s}", .{msg});
}
try s.eval(alloc, self.session.env, text);
return;
}
@@ -552,12 +559,9 @@ pub const Page = struct {
JsErr,
};
// fetchScript senf a GET request to the src and execute the script
// received.
fn fetchScript(self: *Page, src: []const u8) !void {
const alloc = self.arena.allocator();
log.debug("starting fetch script {s}", .{src});
// the caller owns the returned string
fn fetchData(self: *Page, alloc: std.mem.Allocator, src: []const u8) ![]const u8 {
log.debug("starting fetch {s}", .{src});
var buffer: [1024]u8 = undefined;
var b: []u8 = buffer[0..];
@@ -568,46 +572,91 @@ pub const Page = struct {
const resp = fetchres.req.response;
log.info("fetch script {any}: {d}", .{ u, resp.status });
log.info("fetch {any}: {d}", .{ u, resp.status });
if (resp.status != .ok) return FetchError.BadStatusCode;
// TODO check content-type
const body = try fetchres.req.reader().readAllAlloc(alloc, 16 * 1024 * 1024);
defer alloc.free(body);
// check no body
if (body.len == 0) return FetchError.NoBody;
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(self.session.env);
defer try_catch.deinit();
return body;
}
const res = self.session.env.exec(body, src) catch {
if (try try_catch.err(alloc, self.session.env)) |msg| {
defer alloc.free(msg);
log.info("eval remote {s}: {s}", .{ src, msg });
}
return FetchError.JsErr;
// fetchScript senf a GET request to the src and execute the script
// received.
fn fetchScript(self: *Page, s: Script) !void {
const alloc = self.arena.allocator();
const body = try self.fetchData(alloc, s.src);
defer alloc.free(body);
try s.eval(alloc, self.session.env, body);
}
const Script = struct {
element: *parser.Element,
kind: Kind,
isasync: bool,
src: []const u8,
const Kind = enum {
unknown,
javascript,
module,
};
if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, self.session.env);
defer alloc.free(msg);
log.debug("eval remote {s}: {s}", .{ src, msg });
fn init(e: *parser.Element) !?Script {
// ignore non-script tags
const tag = try parser.elementHTMLGetTagType(@as(*parser.ElementHTML, @ptrCast(e)));
if (tag != .script) return null;
return .{
.element = e,
.kind = kind(try parser.elementGetAttribute(e, "type")),
.isasync = try parser.elementGetAttribute(e, "async") != null,
.src = try parser.elementGetAttribute(e, "src") orelse "inline",
};
}
}
// > type
// > Attribute is not set (default), an empty string, or a JavaScript MIME
// > type indicates that the script is a "classic script", containing
// > JavaScript code.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
fn isJS(stype: ?[]const u8) bool {
if (stype == null or stype.?.len == 0) return true;
if (std.mem.eql(u8, stype.?, "application/javascript")) return true;
if (!std.mem.eql(u8, stype.?, "module")) return true;
// > type
// > Attribute is not set (default), an empty string, or a JavaScript MIME
// > type indicates that the script is a "classic script", containing
// > JavaScript code.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attribute_is_not_set_default_an_empty_string_or_a_javascript_mime_type
fn kind(stype: ?[]const u8) Kind {
if (stype == null or stype.?.len == 0) return .javascript;
if (std.mem.eql(u8, stype.?, "application/javascript")) return .javascript;
if (std.mem.eql(u8, stype.?, "module")) return .module;
return false;
}
return .unknown;
}
fn eval(self: Script, alloc: std.mem.Allocator, env: Env, body: []const u8) !void {
var try_catch: jsruntime.TryCatch = undefined;
try_catch.init(env);
defer try_catch.deinit();
const res = switch (self.kind) {
.unknown => return error.UnknownScript,
.javascript => env.exec(body, self.src),
.module => env.module(body, self.src),
} catch {
if (try try_catch.err(alloc, env)) |msg| {
defer alloc.free(msg);
log.info("eval script {s}: {s}", .{ self.src, msg });
}
return FetchError.JsErr;
};
if (builtin.mode == .Debug) {
const msg = try res.toString(alloc, env);
defer alloc.free(msg);
log.debug("eval script {s}: {s}", .{ self.src, msg });
}
}
};
};

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const Client = @import("../http/Client.zig");
const user_agent = "Lightpanda.io/1.0";
const user_agent = @import("browser.zig").user_agent;
pub const Loader = struct {
client: Client,

View File

@@ -112,7 +112,7 @@ fn getWindowForTarget(
const Params = struct {
targetId: ?[]const u8 = null,
};
const input = try Input(Params).get(alloc, msg);
const input = try Input(?Params).get(alloc, msg);
defer input.deinit();
std.debug.assert(input.sessionId != null);
log.debug("Req > id {d}, method {s}", .{ input.id, "browser.getWindowForTarget" });

View File

@@ -31,6 +31,11 @@ const emulation = @import("emulation.zig").emulation;
const fetch = @import("fetch.zig").fetch;
const performance = @import("performance.zig").performance;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const inspector = @import("inspector.zig").inspector;
const dom = @import("dom.zig").dom;
const css = @import("css.zig").css;
const security = @import("security.zig").security;
const log_cdp = std.log.scoped(.cdp);
@@ -59,9 +64,13 @@ const Domains = enum {
Log,
Runtime,
Network,
DOM,
CSS,
Inspector,
Emulation,
Fetch,
Performance,
Security,
};
// The caller is responsible for calling `free` on the returned slice.
@@ -69,12 +78,20 @@ pub fn do(
alloc: std.mem.Allocator,
s: []const u8,
ctx: *Ctx,
) ![]const u8 {
) anyerror![]const u8 {
// incoming message parser
var msg = IncomingMessage.init(alloc, s);
defer msg.deinit();
return dispatch(alloc, &msg, ctx);
}
pub fn dispatch(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) anyerror![]const u8 {
const method = try msg.getMethod();
// retrieve domain from method
@@ -85,21 +102,26 @@ pub fn do(
// select corresponding domain
const action = iter.next() orelse return error.BadMethod;
return switch (domain) {
.Browser => browser(alloc, &msg, action, ctx),
.Target => target(alloc, &msg, action, ctx),
.Page => page(alloc, &msg, action, ctx),
.Log => log(alloc, &msg, action, ctx),
.Runtime => runtime(alloc, &msg, action, ctx),
.Network => network(alloc, &msg, action, ctx),
.Emulation => emulation(alloc, &msg, action, ctx),
.Fetch => fetch(alloc, &msg, action, ctx),
.Performance => performance(alloc, &msg, action, ctx),
.Browser => browser(alloc, msg, action, ctx),
.Target => target(alloc, msg, action, ctx),
.Page => page(alloc, msg, action, ctx),
.Log => log(alloc, msg, action, ctx),
.Runtime => runtime(alloc, msg, action, ctx),
.Network => network(alloc, msg, action, ctx),
.DOM => dom(alloc, msg, action, ctx),
.CSS => css(alloc, msg, action, ctx),
.Inspector => inspector(alloc, msg, action, ctx),
.Emulation => emulation(alloc, msg, action, ctx),
.Fetch => fetch(alloc, msg, action, ctx),
.Performance => performance(alloc, msg, action, ctx),
.Security => security(alloc, msg, action, ctx),
};
}
pub const State = struct {
executionContextId: u32 = 0,
contextID: ?[]const u8 = null,
sessionID: ?[]const u8 = null,
frameID: []const u8 = FrameID,
url: []const u8 = URLBase,
securityOrigin: []const u8 = URLBase,
@@ -200,11 +222,11 @@ pub fn sendEvent(
// ------
// TODO: hard coded IDs
pub const BrowserSessionID = "9559320D92474062597D9875C664CAC0";
pub const ContextSessionID = "4FDC2CB760A23A220497A05C95417CF4";
pub const BrowserSessionID = "BROWSERSESSIONID597D9875C664CAC0";
pub const ContextSessionID = "CONTEXTSESSIONID0497A05C95417CF4";
pub const URLBase = "chrome://newtab/";
pub const FrameID = "90D14BBD8AED408A0467AC93100BCDBE";
pub const LoaderID = "CFC8BED824DD2FD56CF1EF33C965C79C";
pub const LoaderID = "LOADERID24DD2FD56CF1EF33C965C79C";
pub const FrameID = "FRAMEIDD8AED408A0467AC93100BCDBE";
pub const TimestampEvent = struct {
timestamp: f64,

59
src/cdp/css.zig Normal file
View File

@@ -0,0 +1,59 @@
// 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 server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
};
pub fn css(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}

59
src/cdp/dom.zig Normal file
View File

@@ -0,0 +1,59 @@
// 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 server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
};
pub fn dom(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}

59
src/cdp/inspector.zig Normal file
View File

@@ -0,0 +1,59 @@
// 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 server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
};
pub fn inspector(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "inspector.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}

View File

@@ -130,7 +130,8 @@ pub const IncomingMessage = struct {
// asking for getParams, we don't know how to parse them.
fn scanParams(self: *IncomingMessage) !void {
const tt = try self.scanner.peekNextTokenType();
if (tt != .object_begin) return error.InvalidParams;
// accept object begin or null JSON value.
if (tt != .object_begin and tt != .null) return error.InvalidParams;
try self.scanner.skipValue();
self.params_skip = true;
}
@@ -146,10 +147,19 @@ pub const IncomingMessage = struct {
return error.SkippedParams;
}
try self.scanUntil("params");
self.scanUntil("params") catch |err| {
// handle nullable type
if (@typeInfo(T) == .Optional) {
if (err == error.InvalidToken or err == error.EndOfDocument) {
return null;
}
}
return err;
};
// parse "params"
const options = std.json.ParseOptions{
.ignore_unknown_fields = true,
.max_value_len = self.scanner.input.len,
.allocate = .alloc_always,
};
@@ -250,3 +260,34 @@ test "read incoming message with null session id" {
try std.testing.expectEqual(1, try msg.getId());
}
}
test "message with nullable params" {
const T = struct {
bar: []const u8,
};
// nullable type, params is present => value
const not_null =
\\{"id": 1,"method":"foo","params":{"bar":"baz"}}
;
var msg = IncomingMessage.init(std.testing.allocator, not_null);
defer msg.deinit();
const input = try Input(?T).get(std.testing.allocator, &msg);
defer input.deinit();
try std.testing.expectEqualStrings(input.params.?.bar, "baz");
// nullable type, params is not present => null
const is_null =
\\{"id": 1,"method":"foo","sessionId":"AAA"}
;
var msg_null = IncomingMessage.init(std.testing.allocator, is_null);
defer msg_null.deinit();
const input_null = try Input(?T).get(std.testing.allocator, &msg_null);
defer input_null.deinit();
try std.testing.expectEqual(null, input_null.params);
try std.testing.expectEqualStrings("AAA", input_null.sessionId.?);
// not nullable type, params is not present => error
const params_or_error = msg_null.getParams(std.testing.allocator, T);
try std.testing.expectError(error.EndOfDocument, params_or_error);
}

View File

@@ -331,8 +331,9 @@ fn navigate(
// TODO: noop event, we have no env context at this point, is it necesarry?
try sendEvent(alloc, ctx, "Runtime.executionContextsCleared", void, {}, input.sessionId);
// Launch navigate
const p = try ctx.browser.session.createPage();
// Launch navigate, the page must have been created by a
// target.createTarget.
var p = ctx.browser.session.page orelse return error.NoPage;
ctx.state.executionContextId += 1;
const auxData = try std.fmt.allocPrint(
alloc,

View File

@@ -28,6 +28,7 @@ const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const stringify = cdp.stringify;
const target = @import("target.zig");
const log = std.log.scoped(.cdp);
@@ -116,17 +117,25 @@ fn sendInspector(
}
}
ctx.state.sessionID = msg.sessionId;
// remove awaitPromise true params
// TODO: delete when Promise are correctly handled by zig-js-runtime
if (method == .callFunctionOn or method == .evaluate) {
const buf = try alloc.alloc(u8, msg.json.len + 1);
defer alloc.free(buf);
_ = std.mem.replace(u8, msg.json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf);
ctx.sendInspector(buf);
} else {
ctx.sendInspector(msg.json);
if (std.mem.indexOf(u8, msg.json, "\"awaitPromise\":true")) |_| {
const buf = try alloc.alloc(u8, msg.json.len + 1);
defer alloc.free(buf);
_ = std.mem.replace(u8, msg.json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf);
ctx.sendInspector(buf);
return "";
}
}
return "";
ctx.sendInspector(msg.json);
if (msg.id == null) return "";
return result(alloc, msg.id.?, null, null, msg.sessionId);
}
pub const AuxData = struct {

59
src/cdp/security.zig Normal file
View File

@@ -0,0 +1,59 @@
// 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 server = @import("../server.zig");
const Ctx = server.Ctx;
const cdp = @import("cdp.zig");
const result = cdp.result;
const IncomingMessage = @import("msg.zig").IncomingMessage;
const Input = @import("msg.zig").Input;
const log = std.log.scoped(.cdp);
const Methods = enum {
enable,
};
pub fn security(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
action: []const u8,
ctx: *Ctx,
) ![]const u8 {
const method = std.meta.stringToEnum(Methods, action) orelse
return error.UnknownMethod;
return switch (method) {
.enable => enable(alloc, msg, ctx),
};
}
fn enable(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "security.enable" });
return result(alloc, input.id, null, null, input.sessionId);
}

View File

@@ -38,6 +38,8 @@ const Methods = enum {
disposeBrowserContext,
createTarget,
closeTarget,
sendMessageToTarget,
detachFromTarget,
};
pub fn target(
@@ -58,13 +60,15 @@ pub fn target(
.disposeBrowserContext => disposeBrowserContext(alloc, msg, ctx),
.createTarget => createTarget(alloc, msg, ctx),
.closeTarget => closeTarget(alloc, msg, ctx),
.sendMessageToTarget => sendMessageToTarget(alloc, msg, ctx),
.detachFromTarget => detachFromTarget(alloc, msg, ctx),
};
}
// TODO: hard coded IDs
const PageTargetID = "CFCD6EC01573CF29BB638E9DC0F52DDC";
const BrowserTargetID = "2d2bdef9-1c95-416f-8c0e-83f3ab73a30c";
const BrowserContextID = "65618675CB7D3585A95049E9DFE95EA9";
pub const PageTargetID = "PAGETARGETIDB638E9DC0F52DDC";
pub const BrowserTargetID = "browser9-targ-et6f-id0e-83f3ab73a30c";
pub const BrowserContextID = "BROWSERCONTEXTIDA95049E9DFE95EA9";
// TODO: noop method
fn setDiscoverTargets(
@@ -95,6 +99,19 @@ const AttachToTarget = struct {
waitingForDebugger: bool = false,
};
const TargetCreated = struct {
sessionId: []const u8,
targetInfo: struct {
targetId: []const u8,
type: []const u8 = "page",
title: []const u8,
url: []const u8,
attached: bool = true,
canAccessOpener: bool = false,
browserContextId: []const u8,
},
};
const TargetFilter = struct {
type: ?[]const u8 = null,
exclude: ?bool = null,
@@ -123,7 +140,7 @@ fn setAutoAttach(
.sessionId = cdp.BrowserSessionID,
.targetInfo = .{
.targetId = PageTargetID,
.title = "New Incognito tab",
.title = "about:blank",
.url = cdp.URLBase,
.browserContextId = BrowserContextID,
},
@@ -156,8 +173,8 @@ fn attachToTarget(
const attached = AttachToTarget{
.sessionId = cdp.BrowserSessionID,
.targetInfo = .{
.targetId = PageTargetID,
.title = "New Incognito tab",
.targetId = input.params.targetId,
.title = "about:blank",
.url = cdp.URLBase,
.browserContextId = BrowserContextID,
},
@@ -170,7 +187,7 @@ fn attachToTarget(
sessionId: []const u8,
};
const output = SessionId{
.sessionId = input.sessionId orelse BrowserContextID,
.sessionId = input.sessionId orelse cdp.BrowserSessionID,
};
return result(alloc, input.id, SessionId, output, null);
}
@@ -184,7 +201,7 @@ fn getTargetInfo(
const Params = struct {
targetId: ?[]const u8 = null,
};
const input = try Input(Params).get(alloc, msg);
const input = try Input(?Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.getTargetInfo" });
@@ -237,7 +254,7 @@ fn getBrowserContexts(
return result(alloc, input.id, Resp, resp, null);
}
const ContextID = "22648B09EDCCDD11109E2D4FEFBE4F89";
const ContextID = "CONTEXTIDDCCDD11109E2D4FEFBE4F89";
// TODO: noop method
fn createBrowserContext(
@@ -298,8 +315,8 @@ fn disposeBrowserContext(
}
// TODO: hard coded IDs
const TargetID = "57356548460A8F29706A2ADF14316298";
const LoaderID = "DD4A76F842AA389647D702B4D805F49A";
const TargetID = "TARGETID460A8F29706A2ADF14316298";
const LoaderID = "LOADERID42AA389647D702B4D805F49A";
fn createTarget(
alloc: std.mem.Allocator,
@@ -327,15 +344,46 @@ fn createTarget(
ctx.state.securityOrigin = "://";
ctx.state.secureContextType = "InsecureScheme";
ctx.state.loaderID = LoaderID;
ctx.state.sessionID = msg.sessionId;
// TODO stop the previous page instead?
if (ctx.browser.session.page != null) return error.pageAlreadyExists;
// create the page
const p = try ctx.browser.session.createPage();
ctx.state.executionContextId += 1;
// start the js env
const auxData = try std.fmt.allocPrint(
alloc,
// NOTE: we assume this is the default web page
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
.{ctx.state.frameID},
);
defer alloc.free(auxData);
try p.start(auxData);
// send targetCreated event
const created = TargetCreated{
.sessionId = cdp.ContextSessionID,
.targetInfo = .{
.targetId = ctx.state.frameID,
.title = "about:blank",
.url = ctx.state.url,
.browserContextId = input.params.browserContextId orelse ContextID,
.attached = true,
},
};
try cdp.sendEvent(alloc, ctx, "Target.targetCreated", TargetCreated, created, input.sessionId);
// send attachToTarget event
const attached = AttachToTarget{
.sessionId = cdp.ContextSessionID,
.targetInfo = .{
.targetId = ctx.state.frameID,
.title = "",
.title = "about:blank",
.url = ctx.state.url,
.browserContextId = input.params.browserContextId orelse ContextID,
.attached = true,
},
.waitingForDebugger = true,
};
@@ -410,5 +458,66 @@ fn closeTarget(
null,
);
if (ctx.browser.session.page != null) ctx.browser.session.page.?.end();
return "";
}
fn sendMessageToTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
ctx: *Ctx,
) ![]const u8 {
// input
const Params = struct {
message: []const u8,
sessionId: []const u8,
};
const input = try Input(Params).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s} ({s})", .{ input.id, "target.sendMessageToTarget", input.params.message });
// get the wrapped message.
var wmsg = IncomingMessage.init(alloc, input.params.message);
defer wmsg.deinit();
const res = cdp.dispatch(alloc, &wmsg, ctx) catch |e| {
log.err("send message {d} ({s}): {any}", .{ input.id, input.params.message, e });
// TODO dispatch error correctly.
return e;
};
// receivedMessageFromTarget event
const ReceivedMessageFromTarget = struct {
message: []const u8,
sessionId: []const u8,
};
try cdp.sendEvent(
alloc,
ctx,
"Target.receivedMessageFromTarget",
ReceivedMessageFromTarget,
.{
.message = res,
.sessionId = input.params.sessionId,
},
null,
);
return "";
}
// noop
fn detachFromTarget(
alloc: std.mem.Allocator,
msg: *IncomingMessage,
_: *Ctx,
) ![]const u8 {
// input
const input = try Input(void).get(alloc, msg);
defer input.deinit();
log.debug("Req > id {d}, method {s}", .{ input.id, "target.detachFromTarget" });
// output
return result(alloc, input.id, bool, true, input.sessionId);
}

View File

@@ -29,6 +29,7 @@ const EventHandler = @import("../events/event.zig").EventHandler;
const DOMException = @import("exceptions.zig").DOMException;
const Nod = @import("node.zig");
const Window = @import("../html/window.zig").Window;
// EventTarget interfaces
pub const Union = Nod.Union;
@@ -40,9 +41,10 @@ pub const EventTarget = struct {
pub const mem_guarantied = true;
pub fn toInterface(et: *parser.EventTarget) !Union {
// NOTE: for now we state that all EventTarget are Nodes
// TODO: handle other types (eg. Window)
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
return switch (try parser.eventTargetGetType(et)) {
.window => .{ .Window = @as(*Window, @ptrCast(et)) },
.node => Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))),
};
}
// JS funcs

View File

@@ -49,7 +49,7 @@ pub const Stream = struct {
}
fn closeCDP(self: *const Stream) void {
const close_msg: []const u8 = .{ 5, 0 } ++ "close";
const close_msg: []const u8 = .{ 5, 0, 0, 0 } ++ "close";
self.recv(close_msg) catch |err| {
log.err("stream close error: {any}", .{err});
};
@@ -87,7 +87,7 @@ pub const Handler = struct {
}
pub fn clientMessage(self: *Handler, data: []const u8) !void {
var header: [2]u8 = undefined;
var header: [4]u8 = undefined;
Msg.setSize(data.len, &header);
try self.stream.recv(&header);
try self.stream.recv(data);

128
src/html/history.zig Normal file
View File

@@ -0,0 +1,128 @@
// 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 builtin = @import("builtin");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
pub const History = struct {
pub const mem_guarantied = true;
const ScrollRestorationMode = enum {
auto,
manual,
};
scrollRestoration: ScrollRestorationMode = .auto,
state: std.json.Value = .null,
// count tracks the history length until we implement correctly pushstate.
count: u32 = 0,
pub fn get_length(self: *History) u32 {
// TODO return the real history length value.
return self.count;
}
pub fn get_scrollRestoration(self: *History) []const u8 {
return switch (self.scrollRestoration) {
.auto => "auto",
.manual => "manual",
};
}
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
if (std.mem.eql(u8, "manual", mode)) self.scrollRestoration = .manual;
if (std.mem.eql(u8, "auto", mode)) self.scrollRestoration = .auto;
}
pub fn get_state(self: *History) std.json.Value {
return self.state;
}
// TODO implement the function
// data must handle any argument. We could expect a std.json.Value but
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
pub fn _pushState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
self.count += 1;
_ = url;
_ = data;
}
// TODO implement the function
// data must handle any argument. We could expect a std.json.Value but
// https://github.com/lightpanda-io/zig-js-runtime/issues/267 is missing.
pub fn _replaceState(self: *History, data: []const u8, _: ?[]const u8, url: ?[]const u8) void {
_ = self;
_ = url;
_ = data;
}
// TODO implement the function
pub fn _go(self: *History, delta: ?i32) void {
_ = self;
_ = delta;
}
// TODO implement the function
pub fn _back(self: *History) void {
_ = self;
}
// TODO implement the function
pub fn _forward(self: *History) void {
_ = self;
}
};
// Tests
// -----
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var history = [_]Case{
.{ .src = "history.scrollRestoration", .ex = "auto" },
.{ .src = "history.scrollRestoration = 'manual'", .ex = "manual" },
.{ .src = "history.scrollRestoration = 'foo'", .ex = "foo" },
.{ .src = "history.scrollRestoration", .ex = "manual" },
.{ .src = "history.scrollRestoration = 'auto'", .ex = "auto" },
.{ .src = "history.scrollRestoration", .ex = "auto" },
.{ .src = "history.state", .ex = "null" },
.{ .src = "history.pushState({}, null, '')", .ex = "undefined" },
.{ .src = "history.replaceState({}, null, '')", .ex = "undefined" },
.{ .src = "history.go()", .ex = "undefined" },
.{ .src = "history.go(1)", .ex = "undefined" },
.{ .src = "history.go(-1)", .ex = "undefined" },
.{ .src = "history.forward()", .ex = "undefined" },
.{ .src = "history.back()", .ex = "undefined" },
};
try checkCases(js_env, &history);
}

View File

@@ -21,6 +21,8 @@ const generate = @import("../generate.zig");
const HTMLDocument = @import("document.zig").HTMLDocument;
const HTMLElem = @import("elements.zig");
const Window = @import("window.zig").Window;
const Navigator = @import("navigator.zig").Navigator;
const History = @import("history.zig").History;
pub const Interfaces = generate.Tuple(.{
HTMLDocument,
@@ -28,4 +30,6 @@ pub const Interfaces = generate.Tuple(.{
HTMLElem.HTMLMediaElement,
HTMLElem.Interfaces,
Window,
Navigator,
History,
});

102
src/html/navigator.zig Normal file
View File

@@ -0,0 +1,102 @@
// 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 builtin = @import("builtin");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
// https://html.spec.whatwg.org/multipage/system-state.html#navigator
pub const Navigator = struct {
pub const mem_guarantied = true;
agent: []const u8 = "Lightpanda/1.0",
version: []const u8 = "1.0",
vendor: []const u8 = "",
platform: []const u8 = std.fmt.comptimePrint("{any} {any}", .{ builtin.os.tag, builtin.cpu.arch }),
language: []const u8 = "en-US",
pub fn get_userAgent(self: *Navigator) []const u8 {
return self.agent;
}
pub fn get_appCodeName(_: *Navigator) []const u8 {
return "Mozilla";
}
pub fn get_appName(_: *Navigator) []const u8 {
return "Netscape";
}
pub fn get_appVersion(self: *Navigator) []const u8 {
return self.version;
}
pub fn get_platform(self: *Navigator) []const u8 {
return self.platform;
}
pub fn get_product(_: *Navigator) []const u8 {
return "Gecko";
}
pub fn get_productSub(_: *Navigator) []const u8 {
return "20030107";
}
pub fn get_vendor(self: *Navigator) []const u8 {
return self.vendor;
}
pub fn get_vendorSub(_: *Navigator) []const u8 {
return "";
}
pub fn get_language(self: *Navigator) []const u8 {
return self.language;
}
// TODO wait for arrays.
//pub fn get_languages(self: *Navigator) [][]const u8 {
// return .{self.language};
//}
pub fn get_online(_: *Navigator) bool {
return true;
}
pub fn _registerProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
_ = scheme;
_ = url;
}
pub fn _unregisterProtocolHandler(_: *Navigator, scheme: []const u8, url: []const u8) void {
_ = scheme;
_ = url;
}
pub fn get_cookieEnabled(_: *Navigator) bool {
return true;
}
};
// Tests
// -----
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var navigator = [_]Case{
.{ .src = "navigator.userAgent", .ex = "Lightpanda/1.0" },
.{ .src = "navigator.appVersion", .ex = "1.0" },
.{ .src = "navigator.language", .ex = "en-US" },
};
try checkCases(js_env, &navigator);
}

View File

@@ -25,6 +25,8 @@ const CallbackArg = jsruntime.CallbackArg;
const Loop = jsruntime.Loop;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const Navigator = @import("navigator.zig").Navigator;
const History = @import("history.zig").History;
const storage = @import("../storage/storage.zig");
@@ -40,6 +42,7 @@ pub const Window = struct {
document: ?*parser.DocumentHTML = null,
target: []const u8,
history: History = .{},
storageShelf: ?*storage.Shelf = null,
@@ -48,9 +51,13 @@ pub const Window = struct {
timeoutid: u32 = 0,
timeoutids: [512]u64 = undefined,
pub fn create(target: ?[]const u8) Window {
navigator: Navigator,
pub fn create(target: ?[]const u8, navigator: ?Navigator) Window {
return Window{
.target = target orelse "",
.navigator = navigator orelse .{},
.base = .{ .et_type = @intFromEnum(parser.EventTargetType.window) },
};
}
@@ -66,6 +73,10 @@ pub const Window = struct {
return self;
}
pub fn get_navigator(self: *Window) *Navigator {
return &self.navigator;
}
pub fn get_self(self: *Window) *Window {
return self;
}
@@ -78,6 +89,10 @@ pub const Window = struct {
return self.document;
}
pub fn get_history(self: *Window) *History {
return &self.history;
}
pub fn get_name(self: *Window) []const u8 {
return self.target;
}

View File

@@ -40,6 +40,14 @@ pub const websocket_blocking = true;
const log = std.log.scoped(.cli);
pub const std_options = .{
// Set the log level to info
.log_level = .debug,
// Define logFn to override the std implementation
.logFn = logFn,
};
const usage =
\\usage: {s} [options] [URL]
\\
@@ -49,6 +57,7 @@ const usage =
\\ * otherwhise the browser starts a CDP server
\\
\\ -h, --help Print this help message and exit.
\\ --verbose Display all logs. By default only info, warn and err levels are displayed.
\\ --host Host of the CDP server (default "127.0.0.1")
\\ --port Port of the CDP server (default "9222")
\\ --timeout Timeout for incoming connections of the CDP server (in seconds, default "3")
@@ -110,6 +119,10 @@ const CliMode = union(CliModeTag) {
if (std.mem.eql(u8, "-h", opt) or std.mem.eql(u8, "--help", opt)) {
return printUsageExit(execname, 0);
}
if (std.mem.eql(u8, "--verbose", opt)) {
verbose = true;
continue;
}
if (std.mem.eql(u8, "--dump", opt)) {
_fetch.dump = true;
continue;
@@ -313,6 +326,8 @@ pub fn main() !void {
// page
const page = try browser.session.createPage();
try page.start(null);
defer page.end();
_ = page.navigate(opts.url, null) catch |err| switch (err) {
error.UnsupportedUriScheme, error.UriMissingHost => {
@@ -334,3 +349,18 @@ pub fn main() !void {
},
}
}
var verbose: bool = builtin.mode == .Debug; // In debug mode, force verbose.
fn logFn(
comptime level: std.log.Level,
comptime scope: @Type(.EnumLiteral),
comptime format: []const u8,
args: anytype,
) void {
if (!verbose) {
// hide all messages with level greater of equal to debug level.
if (@intFromEnum(level) >= @intFromEnum(std.log.Level.debug)) return;
}
// default std log function.
std.log.defaultLog(level, scope, format, args);
}

View File

@@ -54,7 +54,7 @@ fn execJS(
defer storageShelf.deinit();
// alias global as self and window
var window = Window.create(null);
var window = Window.create(null, null);
window.replaceDocument(doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(window);

View File

@@ -18,17 +18,20 @@
const std = @import("std");
pub const MsgSize = 16 * 1204; // 16KB
pub const HeaderSize = 2;
pub const HeaderSize = 4;
pub const MsgSize = 256 * 1204; // 256KB
// NOTE: Theorically we could go up to 4GB with a 4 bytes binary encoding
// but we prefer to put a lower hard limit for obvious memory size reasons.
pub const MaxSize = HeaderSize + MsgSize;
pub const Msg = struct {
pub fn getSize(data: []const u8) usize {
return std.mem.readInt(u16, data[0..HeaderSize], .little);
return std.mem.readInt(u32, data[0..HeaderSize], .little);
}
pub fn setSize(len: usize, header: *[2]u8) void {
std.mem.writeInt(u16, header, @intCast(len), .little);
pub fn setSize(len: usize, header: *[4]u8) void {
std.mem.writeInt(u32, header, @intCast(len), .little);
}
};
@@ -121,26 +124,26 @@ test "Buffer" {
const cases = [_]Case{
// simple
.{ .input = .{ 2, 0 } ++ "ok", .nb = 1 },
.{ .input = .{ 2, 0, 0, 0 } ++ "ok", .nb = 1 },
// combined
.{ .input = .{ 2, 0 } ++ "ok" ++ .{ 3, 0 } ++ "foo", .nb = 2 },
.{ .input = .{ 2, 0, 0, 0 } ++ "ok" ++ .{ 3, 0, 0, 0 } ++ "foo", .nb = 2 },
// multipart
.{ .input = .{ 9, 0 } ++ "multi", .nb = 0 },
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
.{ .input = "part", .nb = 1 },
// multipart & combined
.{ .input = .{ 9, 0 } ++ "multi", .nb = 0 },
.{ .input = "part" ++ .{ 2, 0 } ++ "ok", .nb = 2 },
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
.{ .input = "part" ++ .{ 2, 0, 0, 0 } ++ "ok", .nb = 2 },
// multipart & combined with other multipart
.{ .input = .{ 9, 0 } ++ "multi", .nb = 0 },
.{ .input = "part" ++ .{ 8, 0 } ++ "co", .nb = 1 },
.{ .input = .{ 9, 0, 0, 0 } ++ "multi", .nb = 0 },
.{ .input = "part" ++ .{ 8, 0, 0, 0 } ++ "co", .nb = 1 },
.{ .input = "mbined", .nb = 1 },
// several multipart
.{ .input = .{ 23, 0 } ++ "multi", .nb = 0 },
.{ .input = .{ 23, 0, 0, 0 } ++ "multi", .nb = 0 },
.{ .input = "several", .nb = 0 },
.{ .input = "complex", .nb = 0 },
.{ .input = "part", .nb = 1 },
// combined & multipart
.{ .input = .{ 2, 0 } ++ "ok" ++ .{ 9, 0 } ++ "multi", .nb = 1 },
.{ .input = .{ 2, 0, 0, 0 } ++ "ok" ++ .{ 9, 0, 0, 0 } ++ "multi", .nb = 1 },
.{ .input = "part", .nb = 1 },
};

View File

@@ -560,6 +560,11 @@ fn eventListenerGetData(lst: *EventListener) ?*anyopaque {
}
// EventTarget
pub const EventTargetType = enum(u4) {
node = c.DOM_EVENT_TARGET_NODE,
window = 2,
};
pub const EventTarget = c.dom_event_target;
pub fn eventTargetToNode(et: *EventTarget) *Node {
@@ -801,6 +806,14 @@ pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool {
return res;
}
pub fn eventTargetGetType(et: *EventTarget) !EventTargetType {
var res: c.dom_event_target_type = undefined;
const err = eventTargetVtable(et).dom_event_target_get_type.?(et, &res);
try DOMErr(err);
return @enumFromInt(res);
}
pub fn eventTargetTBaseFieldName(comptime T: type) ?[]const u8 {
std.debug.assert(@inComptime());
switch (@typeInfo(T)) {
@@ -824,8 +837,10 @@ pub const EventTargetTBase = extern struct {
.remove_event_listener = remove_event_listener,
.add_event_listener = add_event_listener,
.iter_event_listener = iter_event_listener,
.dom_event_target_get_type = dom_event_target_get_type,
},
eti: c.dom_event_target_internal = c.dom_event_target_internal{ .listeners = null },
et_type: c.dom_event_target_type = @intFromEnum(EventTargetType.node),
pub fn add_event_listener(et: [*c]c.dom_event_target, t: [*c]c.dom_string, l: ?*c.struct_dom_event_listener, capture: bool) callconv(.C) c.dom_exception {
const self = @as(*Self, @ptrCast(et));
@@ -858,6 +873,17 @@ pub const EventTargetTBase = extern struct {
const self = @as(*Self, @ptrCast(et));
return c._dom_event_target_iter_event_listener(self.eti, t, capture, cur, next, l);
}
pub fn dom_event_target_get_type(
et: [*c]c.dom_event_target,
res: [*c]c.dom_event_target_type,
) callconv(.C) c.dom_exception {
const self = @as(*Self, @ptrCast(et));
res.* = self.et_type;
return c.DOM_NO_ERR;
}
};
// NodeType

View File

@@ -96,7 +96,7 @@ fn testExecFn(
});
// alias global as self and window
var window = Window.create(null);
var window = Window.create(null, null);
window.replaceDocument(doc);
window.setStorageShelf(&storageShelf);
@@ -137,6 +137,8 @@ fn testsAllExecFn(
HTMLElementTestExecFn,
MutationObserverTestExecFn,
@import("polyfill/fetch.zig").testExecFn,
@import("html/navigator.zig").testExecFn,
@import("html/history.zig").testExecFn,
};
inline for (testFns) |testFn| {
@@ -359,7 +361,7 @@ test "bug document html parsing #4" {
}
test "Window is a libdom event target" {
var window = Window.create(null);
var window = Window.create(null, null);
const event = try parser.eventCreate();
try parser.eventInit(event, "foo", .{});

View File

@@ -175,6 +175,7 @@ pub const Ctx = struct {
self.do(parts.msg) catch |err| {
if (err != error.Closed) {
log.err("do error: {any}", .{err});
log.debug("last msg: {s}", .{parts.msg});
}
};
}
@@ -347,7 +348,7 @@ pub const Ctx = struct {
const s = try std.fmt.allocPrint(
allocator,
tpl,
.{ msg_open, cdp.ContextSessionID },
.{ msg_open, ctx.state.sessionID orelse cdp.ContextSessionID },
);
try ctx.send(s);

View File

@@ -173,7 +173,7 @@ pub const Bottle = struct {
// > context of another document.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
//
// So for now, we won't impement the feature.
// So for now, we won't implement the feature.
}
pub fn _removeItem(self: *Bottle, k: []const u8) !void {

View File

@@ -90,7 +90,7 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
}
// setup global env vars.
var window = Window.create(null);
var window = Window.create(null, null);
window.replaceDocument(html_doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(&window);