Compare commits

..

78 Commits

Author SHA1 Message Date
Pierre Tachoire
d499a7931c percent encode version query string for crash report 2026-04-01 17:53:25 +02:00
Nikolay Govorov
e79da3a4ad Merge pull request #2064 from lightpanda-io/network_naming
Improve network naming consistency
2026-04-01 13:10:12 +01:00
Karl Seguin
145792c4f5 Merge pull request #2061 from lightpanda-io/ariaAtomic
Add Element.ariaAtomic and Element.ariaLive properties
2026-04-01 20:06:15 +08:00
Karl Seguin
0bb3e3827d Merge pull request #2060 from lightpanda-io/HTMLAnchorElement.rel
Add HTMLAnchorElement.rel property
2026-04-01 20:04:43 +08:00
Karl Seguin
6e6e6e6fad Merge pull request #2057 from lightpanda-io/element-title
Add HTMLElement.title property
2026-04-01 19:36:12 +08:00
Karl Seguin
9d13a7ccdb Merge pull request #2065 from lightpanda-io/browser/resolve-scheme-in-path
Browser/resolve scheme in path
2026-04-01 19:30:40 +08:00
Karl Seguin
7fcaa500d8 Fix typo in variable name
protect against overflow if path stats with ':'

Minor tweaks to https://github.com/lightpanda-io/browser/pull/2046
2026-04-01 19:20:55 +08:00
Karl Seguin
0604056f76 Improve network naming consistency
1.
Runtime.zig -> Network.zig (especially since most places imported it as
`const Network = @import("Runtime.zig")`

2.
const net_http = @import(...) -> const http = @import(...)
2026-04-01 18:46:03 +08:00
Pierre Tachoire
5965d37c79 Add HTMLAnchorElement.rel property
Reflects the `rel` HTML attribute. The `relList` DOMTokenList was
already implemented but the string `rel` accessor was missing.
2026-04-01 11:15:10 +02:00
Pierre Tachoire
e430051fff Add Element.ariaAtomic and Element.ariaLive properties
ARIAMixin attribute reflection on Element, per the ARIA spec.
2026-04-01 11:13:52 +02:00
Pierre Tachoire
ca8361f5c1 Merge pull request #2059 from lightpanda-io/integration-e2e-report
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
ci: send e2e integration results to slack
2026-04-01 10:41:16 +02:00
Pierre Tachoire
bc4afcd82f Merge pull request #2058 from lightpanda-io/wpt-diff
Wpt diff
2026-04-01 10:31:12 +02:00
Pierre Tachoire
1c1bd81daa ci: send e2e integration results to slack 2026-04-01 10:29:56 +02:00
Pierre Tachoire
ea87fc2c50 ci: publish ci regression list into slack 2026-04-01 10:18:06 +02:00
Pierre Tachoire
e412dfed2f Add HTMLElement.title property
Reflects the `title` HTML attribute as a getter/setter on HTMLElement.
2026-04-01 09:15:34 +02:00
Karl Seguin
f4802b49e8 Merge pull request #2055 from lightpanda-io/document-dir
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Add HTMLElement.dir and HTMLElement.lang properties
2026-04-01 06:46:06 +08:00
Karl Seguin
a26f544509 Merge pull request #2054 from lightpanda-io/nikneym/url-search-params-array-constructor
`URLSearchParams`: support passing arrays to constructor
2026-04-01 06:45:14 +08:00
dinisimys2018
2d87f5bf47 fix(browser-url): handle specific file scheme and change error InvalidURL to TypeError 2026-03-31 18:42:03 +03:00
Karl Seguin
469a6ecaea Merge pull request #2051 from lightpanda-io/structureClone_host_object
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Provide a failing callback to ValueSerializer for host objects
2026-03-31 22:33:54 +08:00
Pierre Tachoire
68e2140bb3 Add HTMLElement.dir and HTMLElement.lang properties
Added as attribute-backed accessors on HTMLElement (inherited by all
HTML elements) and on HTMLDocument (delegates to documentElement).
2026-03-31 16:12:26 +02:00
dinisimys2018
0a222ff397 fix(browser-url): add more combinations base+path handle 2026-03-31 16:54:06 +03:00
Halil Durak
94b003804b URLSearchParams: update tests 2026-03-31 16:01:21 +03:00
Halil Durak
b41c86e104 URLSearchParams: support passing arrays to constructor 2026-03-31 16:01:08 +03:00
Karl Seguin
4ad8282e75 Merge pull request #2047 from lightpanda-io/fancy-wait
Add --wait-selector, --wait-script and --wait-script-file options to …
2026-03-31 20:59:48 +08:00
Karl Seguin
3d624a97a7 Merge pull request #2052 from lightpanda-io/xhr_expand_ref
Expand the lifetime of the XHR reference
2026-03-31 20:59:28 +08:00
Karl Seguin
571c346316 use releaseSelfRef on done just to be extra safe 2026-03-31 20:39:11 +08:00
Karl Seguin
af19cbc726 use remaining instead of timeout_ms, else, what's the point?
Co-authored-by: Adrià Arrufat <1671644+arrufat@users.noreply.github.com>
2026-03-31 19:42:32 +08:00
Karl Seguin
9716150530 rename has_ref -> active_request 2026-03-31 19:15:01 +08:00
Karl Seguin
20cadd2282 Merge pull request #2053 from lightpanda-io/remove_dead_code
remove dead code
2026-03-31 18:24:48 +08:00
Karl Seguin
4c4f395b1e Merge pull request #2036 from lightpanda-io/remove_cdp_generics
Removing remaining CDP generic
2026-03-31 17:54:34 +08:00
Karl Seguin
26653120fa Removing remaining CDP generic
Follow up to https://github.com/lightpanda-io/browser/pull/1990 which makes
both BrowserContext and Command non-generic.
2026-03-31 16:53:58 +08:00
Karl Seguin
a4a024ce9c remove dead code 2026-03-31 16:48:47 +08:00
Karl Seguin
492fd86bad Expand the lifetime of the XHR reference
We need to take the self-reference to the XHR object as soon as the request is
made. Previously, we were waiting until we got the start callback, but v8 could
(and does) drop the reference before that happens. Unfortunately, that means
we can no longer use _transfer == null to tell if we own a reference or not, so
a new boolean was added.
2026-03-31 16:46:31 +08:00
Karl Seguin
c3c465347d Provide a failing callback to ValueSerializer for host objects
V8 needs our help when serializing host (e.g. a Zig dom instance) objects. We
don't currently have this implemented, so this provides the callback that throws
an error. Our wrapper returns Nothing when no callback is provided, which v8
doesn't allow (via assertion).
2026-03-31 16:13:55 +08:00
Adrià Arrufat
55666ab7cd Merge pull request #2049 from lightpanda-io/refactor/getNodeDetails-param-order
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
SemanticTree: reorder getNodeDetails params
2026-03-31 07:58:28 +02:00
Adrià Arrufat
008235222b SemanticTree: reorder getNodeDetails params 2026-03-31 07:29:33 +02:00
Adrià Arrufat
b03dbc77ab Merge pull request #2048 from lightpanda-io/feature/semantic-tree-checked-state
SemanticTree: add checked state to node data and output
2026-03-31 07:08:36 +02:00
Adrià Arrufat
fc057a3bb3 SemanticTree: add checked state to node data and output 2026-03-31 06:43:01 +02:00
Karl Seguin
ab6c63b24b Add --wait-selector, --wait-script and --wait-script-file options to fetch
These new optional parameter run AFTER --wait-until, allowing the (imo) useful
combination of `--wait-until load --wait-script "report.complete === true"`.
However, if `--wait-until` IS NOT specified but `--wait-selector/script` IS,
then there is no default wait and it'll just check the selector/script. If
neither `--wait-selector` or `--wait-script/--wait-script-file` are specified
 then  `--wait-until` continues to default to `done`.

These waiters were added to the Runner, and the existing Action.waitForSelector
now uses the runner's version. Selector querying has been split into distinct
parse and query functions, so that we can parse once, and query on every tick.

We could potentially optimize --wait-script to compile the script once and call
it on each tick, but we'd have to detect page navigation to recompile the script
in the new context. Something I'd rather optimize separately.
2026-03-31 12:30:46 +08:00
Adrià Arrufat
16f17ead9a Merge pull request #2045 from lightpanda-io/semantic-tree-node-details
SemanticTree: Add nodeDetails tool
2026-03-31 05:28:24 +02:00
Adrià Arrufat
367d20d39f SemanticTree: simplify lp.String.wrap calls 2026-03-31 05:20:32 +02:00
Karl Seguin
a6a8752787 Merge pull request #2032 from lightpanda-io/cdp_navigation_event_order
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (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 / zig build release (push) Has been cancelled
wpt / build wpt runner (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
Improve/Fix CDP navigation event order
2026-03-31 07:12:14 +08:00
Karl Seguin
568fa25add Remove DOMContentLoaded and Loaded events from page_navigated
These were moved to their own distinct events, and should have been removed from
here.
2026-03-31 06:56:01 +08:00
Karl Seguin
752184b12b Improve/Fix CDP navigation event order
These changes all better align with chrome's event ordering/timing.

There are two big changes. The first is that our internal page_navigated event,
which is kind of our heavy hitter, is sent once the header is received as
opposed to (much later) on document load. The main goal of this internal event
is to trigger the "Page.frameNavigated" CDP event which is meant to happen
once the URL is committed, which _is_ on header response.

To accommodate this earlier trigger, new explicit events for DOMContentLoaded
and load have be added.

This drastically changes the flow of events as things go from:
Start Page Navigation
Response Received
  Start Frame Navigation
  Response Received
  End Frame Navigation
End Page Navigation
context clear + reset
DOMContentLoaded
Loaded

TO:
Start Page Navigation
Response Received
End Page Navigation
context clear + reset
Start Frame Navigation
Response Received
End Frame Navigation
DOMContentLoaded
Loaded

So not only does it remove the nesting, but it ensures that the context are
cleared and reset once the main page's navigation is locked in, and before any
frame is created.
2026-03-31 06:56:00 +08:00
dinisimys2018
9a0cefad26 fix(browser-url): url resolve scheme in path 2026-03-30 18:58:19 +03:00
Pierre Tachoire
47afdc003a Merge pull request #2044 from lightpanda-io/proxy-auth-challenge
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
http: add connect code into auth challenge detection
2026-03-30 17:18:56 +02:00
Adrià Arrufat
9c8fe9b20f SemanticTree: Add nodeDetails tool
Adds a tool to retrieve detailed node metadata and updates the
semantic tree to track and display the disabled state of elements.
2026-03-30 16:38:23 +02:00
Pierre Tachoire
7606528b37 ci: fix request interception proxy challenge 2026-03-30 15:32:38 +02:00
Pierre Tachoire
9ca6bf42ae http: add connect headers to auth challenge detection 2026-03-30 15:17:12 +02:00
Pierre Tachoire
a272a2c314 http: add connect code into auth challenge detection 2026-03-30 15:08:36 +02:00
katie-lpd
cafa16d190 Update README.md
Updating benchmark text to fit the image
2026-03-30 13:38:46 +02:00
Pierre Tachoire
a5ed3cdaee Merge pull request #2042 from lightpanda-io/security.md
add SECURITY.md
2026-03-30 11:30:42 +02:00
Pierre Tachoire
b473f0e681 add SECURITY.md 2026-03-30 11:28:46 +02:00
Karl Seguin
be7226fc7b Merge pull request #2024 from lightpanda-io/finalizers
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / wba-demo-scripts (push) Has been cancelled
e2e-test / wba-test (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig fmt (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
Rework finalizers
2026-03-30 17:03:08 +08:00
Karl Seguin
5cd356631c unregister before releasingRef to avoid potential use-after-free 2026-03-30 15:42:08 +08:00
Pierre Tachoire
8723ecdd2d Merge pull request #2028 from lightpanda-io/http_client_safe_kill
Protect transfer.kill() the way transfer.abort() is protected
2026-03-30 09:22:40 +02:00
Pierre Tachoire
451178558a Merge pull request #2026 from lightpanda-io/invalid_access_dom_exception
Add missing InvalidAccessError DOMException mapping
2026-03-30 09:21:51 +02:00
Karl Seguin
70dc0f6b95 Merge pull request #2027 from lightpanda-io/mcp-protocol-version
mcp: allow configuring protocol version
2026-03-30 13:38:06 +08:00
Adrià Arrufat
d99599fa21 zig fmt 2026-03-30 07:24:08 +02:00
Adrià Arrufat
20e62a5551 mcp: inline mcpVersion helper from Config 2026-03-30 07:13:45 +02:00
Adrià Arrufat
e083d4a3d1 Config: remove LIGHTPANDA_MCP_VERSION env var 2026-03-30 07:07:23 +02:00
Karl Seguin
7a23686cbd Merge pull request #2033 from lightpanda-io/canvas_context_cache
Canvas context cache
2026-03-30 12:27:04 +08:00
Karl Seguin
25889ff918 Improve canvas context caching
Improve https://github.com/lightpanda-io/browser/pull/2022 to also cache webgl
context and add tests.
2026-03-30 12:14:32 +08:00
Karl Seguin
b4e3f246ca Merge remote-tracking branch 'evan108108/fix/canvas-getcontext-caching' into canvas_context_cache 2026-03-30 11:58:45 +08:00
Karl Seguin
8eeeeda8c1 Merge pull request #2021 from evan108108/fix/navigator-spec-compliance
fix: navigator.languages should include base language per spec
2026-03-30 11:40:49 +08:00
Karl Seguin
75dc4d5b0e Merge pull request #2031 from lightpanda-io/cdp-add-script-to-evaluate-on-new-document
Cdp add script to evaluate on new document
2026-03-30 11:16:39 +08:00
Karl Seguin
0d40aed1b7 zig fmt 2026-03-30 09:32:22 +08:00
Karl Seguin
78cb766298 Log for unimplemented parameter
Wrap script_on_new_document execution in try/catch for better error reporting.

Improve test for script_on_new_document
2026-03-30 09:31:13 +08:00
Karl Seguin
f60e5cce6d Protect transfer.kill() the way transfer.abort() is protected
Transfer.abort() is protected from aborting the transfer while inside of a
libcurl callback (since libcurl doesn't support mutating the easy while inside
of a callback AND it causes issues in the zig code).

This applies similar logic to Transfer.kill() which is less likely to be called
but worse if it is called in a callback, as transfer.kill() deinit's the
transfer - something the callback caller is not expecting. Since killing isn't
safe to do, we flag the transfer as aborted AND null/noop all the callbacks.

Fixes WPT crash /content-security-policy/frame-src/frame-src-blocked-path-matching.sub.html
2026-03-29 19:48:47 +08:00
Adrià Arrufat
81d4bdb157 mcp: change default protocol version to 2024-11-05 2026-03-29 08:34:24 +02:00
Adrià Arrufat
cf5e4d7d1e mcp: allow configuring protocol version
Closes #2023
2026-03-29 08:29:04 +02:00
Karl Seguin
9f81d7d3ff Add missing InvalidAccessError DOMException mapping
Fixes WPT crash /WebCryptoAPI/sign_verify/eddsa_curve25519.https.any.html
2026-03-29 11:46:44 +08:00
Karl Seguin
269924090a fix double free 2026-03-29 07:12:18 +08:00
Karl Seguin
ad54437ca3 zig fmt 2026-03-28 21:43:46 +08:00
Karl Seguin
01ecb296e5 Rework finalizers
This commit involves a number of changes to finalizers, all aimed towards
better consistency and reliability.

A big part of this has to do with v8::Inspector's ability to move objects
across IsolatedWorlds. There has been a few previous efforts on this, the most
significant being https://github.com/lightpanda-io/browser/pull/1901. To recap,
a Zig instance can map to 0-N v8::Objects. Where N is the total number of
IsolatedWorlds. Generally, IsolatedWorlds between origins are...isolated...but
the v8::Inspector isn't bound by this. So a Zig instance cannot be tied to a
Context/Identity/IsolatedWorld...it has to live until all references, possibly
from different IsolatedWorlds, are released (or the page is reset).

Finalizers could previously be managed via reference counting or explicitly
toggling the instance as weak/strong. Now, only reference counting is supported.
weak/strong can essentially be seen as an acquireRef (rc += 1) and
releaseRef (rc -= 1). Explicit setting did make some things easier, like not
having to worry so much about double-releasing (e.g. XHR abort being called
multiple times), but it was only used in a few places AND it simply doesn't work
with objects shared between IsolatedWorlds. It is never a boolean now, as 3
different IsolatedWorlds can each hold a reference.

Temps and Globals are tracked on the Session. Previously, they were tracked on
the Identity, but that makes no sense. If a Zig instance can outlive an Identity,
then any of its Temp references can too. This hasn't been a problem because we've
only seen MutationObserver and IntersectionObserver be used cross-origin,
but the right CDP script can make this crash with a use-after-free (e.g.
`MessageEvent.data` is released when the Identity is done, but `MessageEvent` is
still referenced by a different IsolateWorld).

Rather than deinit with a `comptime shutdown: bool`, there is now an explicit
`releaseRef` and `deinit`.

Bridge registration has been streamlined. Previously, types had to register
their finalizer AND acquireRef/releaseRef/deinit had to be declared on the entire
prototype chain, even if these methods just delegated to their proto. Finalizers
are now automatically enabled if a type has a `acquireRef` function. If a type
has an `acquireRef`, then it must have a `releaseRef` and a `deinit`. So if
there's custom cleanup to do in `deinit`, then you also have to define
`acquireRef` and `releaseRef` which will just delegate to the _proto.

Furthermore these finalizer methods can be defined anywhere on the chain.

Previously:

```zig
const KeywboardEvent = struct {
  _proto: *Event,
  ...

  pub fn deinit(self: *KeyboardEvent, session: *Session) void {
    self._proto.deinit(session);
  }

  pub fn releaseRef(self: *KeyboardEvent, session: *Session) void {
    self._proto.releaseRef(session);
  }
}
```

```zig
const KeyboardEvent = struct {
  _proto: *Event,
  ...
  // no deinit, releaseRef, acquireref
}
```

Since the `KeyboardEvent` doesn't participate in finalization directly, it
doesn't have to define anything. The bridge will detect the most specific place
they are defined and call them there.
2026-03-28 21:11:23 +08:00
evan108108
1f22462f13 fix: cache canvas 2D context and lock context type per spec
Per the HTML spec, HTMLCanvasElement.getContext() should:
1. Return the same object on repeated calls with the same type
2. Return null if a different context type was already requested

Previously, every getContext("2d") call created a new
CanvasRenderingContext2D object. This caused issues with code
that relies on identity checks (ctx === canvas.getContext("2d"))
and wasted memory by allocating duplicate contexts.

The fix caches the 2D context and tracks which context type was
first requested, returning null for incompatible subsequent calls.
2026-03-27 21:06:09 -04:00
evan108108
273ea91378 fix: navigator.languages should include base language per spec
Per the HTML spec, navigator.languages should return the user's
preferred languages. Most browsers return at least ["en-US", "en"]
to include the base language tag alongside the regional variant.

This matches Chrome, Firefox, and Safari behavior and improves
compatibility with sites that check for language negotiation.
2026-03-27 21:04:55 -04:00
Navid EMAD
886aa3abba CDP: implement Page.addScriptToEvaluateOnNewDocument
Replace the hardcoded stub with a working implementation that stores
registered scripts and evaluates them in each new document.

Changes:
- Add ScriptOnNewDocument struct and storage list on BrowserContext
- Store scripts with unique identifiers when addScript is called
- Evaluate all registered scripts in pageNavigated, after the execution
  context is created but before frameNavigated/loadEventFired events
  are sent to the CDP client
- Add removeScriptToEvaluateOnNewDocument for cleanup
- Return unique identifiers per the CDP spec (was hardcoded to "1")

Scripts are evaluated with error suppression (warns on failure) to
avoid breaking navigation if a script has issues.

This unblocks CDP clients that rely on auto-injected scripts (polyfills,
monitoring, test helpers) persisting across navigations. Previously
clients had to manually re-inject after every Page.navigate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:48:07 +01:00
122 changed files with 2706 additions and 1405 deletions

View File

@@ -60,7 +60,20 @@ jobs:
- run: chmod a+x ./lightpanda
- name: run end to end integration tests
continue-on-error: true
run: |
./lightpanda serve --log-level error & echo $! > LPD.pid
go run integration/main.go
go run integration/main.go |tee result.log
kill `cat LPD.pid`
- name: Send result to slack
uses: slackapi/slack-github-action@v3.0.1
with:
errors: true
method: files.uploadV2
token: ${{ secrets.CI_SLACK_BOT_TOKEN }}
payload: |
channel_id: ${{ vars.E2E_SLACK_CHANNEL_ID }}
initial_comment: "Last e2e integration tests"
file: "./result.log"
filename: "e2e-integration-${{ github.sha }}.txt"

View File

@@ -100,14 +100,14 @@ jobs:
./proxy/proxy & echo $! > PROXY.id
./lightpanda serve --http-proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid
go run runner/main.go
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
kill `cat LPD.pid` `cat PROXY.id`
- name: run request interception through proxy
- name: run request interception through proxy and playwright
run: |
export PROXY_USERNAME=username PROXY_PASSWORD=password
./proxy/proxy & echo $! > PROXY.id
./lightpanda serve & echo $! > LPD.pid
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
kill `cat LPD.pid` `cat PROXY.id`
@@ -161,14 +161,18 @@ jobs:
--http-proxy 'http://127.0.0.1:3000' \
& echo $! > LPD.pid
go run runner/main.go
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
kill `cat LPD.pid` `cat PROXY.id`
- name: run request interception through proxy
- name: run request interception through proxy and playwright
run: |
export PROXY_USERNAME=username PROXY_PASSWORD=password
./proxy/proxy & echo $! > PROXY.id
./lightpanda serve & echo $! > LPD.pid
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
./lightpanda serve \
--web-bot-auth-key-file private_key.pem \
--web-bot-auth-keyid ${{ vars.WBA_KEY_ID }} \
--web-bot-auth-domain ${{ vars.WBA_DOMAIN }} \
& echo $! > LPD.pid
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
kill `cat LPD.pid` `cat PROXY.id`

View File

@@ -153,3 +153,34 @@ jobs:
- name: format and send json result
run: /perf-fmt wpt ${{ github.sha }} wpt.json
wptdiff:
name: perf-fmt
needs: perf-fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
repository: 'lightpanda-io/demo'
fetch-depth: 0
- run: |
cd ./wptdiff
CGO_ENABLED=0 go build
- run: |
./wptdiff/wptdiff |tee diff.log
- name: Send regression to slack
uses: slackapi/slack-github-action@v3.0.1
with:
errors: true
method: files.uploadV2
token: ${{ secrets.CI_SLACK_BOT_TOKEN }}
payload: |
channel_id: ${{ vars.WPT_SLACK_CHANNEL_ID }}
initial_comment: "Last WPT regressions"
file: "./diff.log"
filename: "wpt-regression-${{ github.sha }}.txt"

View File

@@ -36,8 +36,8 @@ Lightpanda is the open-source browser made for headless usage:
Fast web automation for AI agents, LLM training, scraping and testing:
- Ultra-low memory footprint (9x less than Chrome)
- Exceptionally fast execution (11x faster than Chrome)
- Ultra-low memory footprint (16x less than Chrome)
- Exceptionally fast execution (9x faster than Chrome)
- Instant startup
[^1]: **Playwright support disclaimer:**

11
SECURITY.md Normal file
View File

@@ -0,0 +1,11 @@
# Reporting security issues
## Supported Versions
Security fixes are applied to the latest `main` branch.
## Reporting a Vulnerability
Please **DO NOT** file a public issue, instead send your report privately to security@lightpanda.io.
Security reports are greatly appreciated and we will publicly thank you for it, although we keep your name confidential if you request it.

View File

@@ -46,8 +46,12 @@ pub fn build(b: *Build) !void {
var stdout = std.fs.File.stdout().writer(&.{});
try stdout.interface.print("Lightpanda {f}\n", .{version});
const version_string = b.fmt("{f}", .{version});
const version_encoded = std.mem.replaceOwned(u8, b.allocator, version_string, "+", "%2B") catch @panic("OOM");
var opts = b.addOptions();
opts.addOption([]const u8, "version", b.fmt("{f}", .{version}));
opts.addOption([]const u8, "version", version_string);
opts.addOption([]const u8, "version_encoded", version_encoded);
opts.addOption(?[]const u8, "snapshot_path", snapshot_path);
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer") orelse false;

View File

@@ -26,7 +26,7 @@ const Snapshot = @import("browser/js/Snapshot.zig");
const Platform = @import("browser/js/Platform.zig");
const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
const Network = @import("network/Runtime.zig");
const Network = @import("network/Network.zig");
pub const ArenaPool = @import("ArenaPool.zig");
const App = @This();

View File

@@ -24,6 +24,7 @@ const log = @import("log.zig");
const dump = @import("browser/dump.zig");
const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config;
const mcp = @import("mcp.zig");
pub const RunMode = enum {
help,
@@ -33,7 +34,6 @@ pub const RunMode = enum {
mcp,
};
pub const MAX_LISTENERS = 16;
pub const CDP_MAX_HTTP_REQUEST_SIZE = 4096;
// max message size
@@ -222,6 +222,7 @@ pub const Serve = struct {
pub const Mcp = struct {
common: Common = .{},
version: mcp.Version = .default,
};
pub const DumpFormat = enum {
@@ -247,7 +248,9 @@ pub const Fetch = struct {
with_frames: bool = false,
strip: dump.Opts.Strip = .{},
wait_ms: u32 = 5000,
wait_until: WaitUntil = .done,
wait_until: ?WaitUntil = null,
wait_script: ?[:0]const u8 = null,
wait_selector: ?[:0]const u8 = null,
};
pub const Common = struct {
@@ -411,12 +414,24 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
\\
\\--with-frames Includes the contents of iframes. Defaults to false.
\\
\\--wait-ms Wait time in milliseconds.
\\--wait-ms Wait time in milliseconds. Supersedes all other --wait
\\ parameters.
\\ Defaults to 5000.
\\
\\--wait-until Wait until the specified event.
\\ Supported events: load, domcontentloaded, networkidle, done.
\\ Defaults to 'done'.
\\--wait-until Wait until the specified event. Checked before the other
\\ --wait- options. Supported events: load, domcontentloaded,
\\ networkidle, done.
\\ Defaults to 'done'. If --wait-selector, --wait-script or
\\ --wait-script-file are specified, defaults to none.
\\
\\--wait-selector Wait for an element matching the CSS selector to appear.
\\ Checked after --wait-until condition is met.
\\
\\--wait-script Wait for a JavaScript expression to return truthy.
\\ Checked after --wait-until condition is met.
\\
\\--wait-script-file
\\ Like --wait-script, but reads the script from a file.
\\
++ common_options ++
\\
@@ -453,6 +468,12 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
\\Starts an MCP (Model Context Protocol) server over stdio
\\Example: {s} mcp
\\
\\Options:
\\--version
\\ Override the reported MCP version.
\\ Valid: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25.
\\ Defaults to "2024-11-05".
\\
++ common_options ++
\\
\\version command
@@ -640,10 +661,22 @@ fn parseMcpArgs(
allocator: Allocator,
args: *std.process.ArgIterator,
) !Mcp {
var mcp: Mcp = .{};
var result: Mcp = .{};
while (args.next()) |opt| {
if (try parseCommonArg(allocator, opt, args, &mcp.common)) {
if (std.mem.eql(u8, "--version", opt)) {
const str = args.next() orelse {
log.fatal(.mcp, "missing argument value", .{ .arg = opt });
return error.InvalidArgument;
};
result.version = std.meta.stringToEnum(mcp.Version, str) orelse {
log.fatal(.mcp, "invalid protocol version", .{ .value = str });
return error.InvalidArgument;
};
continue;
}
if (try parseCommonArg(allocator, opt, args, &result.common)) {
continue;
}
@@ -651,7 +684,7 @@ fn parseMcpArgs(
return error.UnkownOption;
}
return mcp;
return result;
}
fn parseFetchArgs(
@@ -665,7 +698,9 @@ fn parseFetchArgs(
var common: Common = .{};
var strip: dump.Opts.Strip = .{};
var wait_ms: u32 = 5000;
var wait_until: WaitUntil = .done;
var wait_until: ?WaitUntil = null;
var wait_script: ?[:0]const u8 = null;
var wait_selector: ?[:0]const u8 = null;
while (args.next()) |opt| {
if (std.mem.eql(u8, "--wait-ms", opt) or std.mem.eql(u8, "--wait_ms", opt)) {
@@ -692,6 +727,36 @@ fn parseFetchArgs(
continue;
}
if (std.mem.eql(u8, "--wait-selector", opt) or std.mem.eql(u8, "--wait_selector", opt)) {
const str = args.next() orelse {
log.fatal(.app, "missing argument value", .{ .arg = opt });
return error.InvalidArgument;
};
wait_selector = try allocator.dupeZ(u8, str);
continue;
}
if (std.mem.eql(u8, "--wait-script", opt) or std.mem.eql(u8, "--wait_script", opt)) {
const str = args.next() orelse {
log.fatal(.app, "missing argument value", .{ .arg = opt });
return error.InvalidArgument;
};
wait_script = try allocator.dupeZ(u8, str);
continue;
}
if (std.mem.eql(u8, "--wait-script-file", opt) or std.mem.eql(u8, "--wait_script_file", opt)) {
const path = args.next() orelse {
log.fatal(.app, "missing argument value", .{ .arg = opt });
return error.InvalidArgument;
};
wait_script = std.fs.cwd().readFileAllocOptions(allocator, path, 1024 * 1024, null, .of(u8), 0) catch |err| {
log.fatal(.app, "failed to read file", .{ .arg = opt, .path = path, .err = err });
return error.InvalidArgument;
};
continue;
}
if (std.mem.eql(u8, "--dump", opt)) {
var peek_args = args.*;
if (peek_args.next()) |next_arg| {
@@ -782,6 +847,8 @@ fn parseFetchArgs(
.with_frames = with_frames,
.wait_ms = wait_ms,
.wait_until = wait_until,
.wait_selector = wait_selector,
.wait_script = wait_script,
};
}

View File

@@ -74,6 +74,8 @@ const EventListeners = struct {
page_network_idle: List = .{},
page_network_almost_idle: List = .{},
page_frame_created: List = .{},
page_dom_content_loaded: List = .{},
page_loaded: List = .{},
http_request_fail: List = .{},
http_request_start: List = .{},
http_request_intercept: List = .{},
@@ -91,6 +93,8 @@ const Events = union(enum) {
page_network_idle: *const PageNetworkIdle,
page_network_almost_idle: *const PageNetworkAlmostIdle,
page_frame_created: *const PageFrameCreated,
page_dom_content_loaded: *const PageDOMContentLoaded,
page_loaded: *const PageLoaded,
http_request_fail: *const RequestFail,
http_request_start: *const RequestStart,
http_request_intercept: *const RequestIntercept,
@@ -137,6 +141,18 @@ pub const PageFrameCreated = struct {
timestamp: u64,
};
pub const PageDOMContentLoaded = struct {
req_id: u32,
frame_id: u32,
timestamp: u64,
};
pub const PageLoaded = struct {
req_id: u32,
frame_id: u32,
timestamp: u64,
};
pub const RequestStart = struct {
transfer: *Transfer,
};

View File

@@ -95,9 +95,11 @@ const NodeData = struct {
name: ?[]const u8,
value: ?[]const u8,
options: ?[]OptionData = null,
checked: ?bool = null,
xpath: []const u8,
is_interactive: bool,
node_name: []const u8,
interactive: bool,
disabled: bool,
tag_name: []const u8,
};
const WalkContext = struct {
@@ -148,16 +150,21 @@ fn walk(
const role = try axn.getRole();
var is_interactive = false;
var is_disabled = false;
var value: ?[]const u8 = null;
var options: ?[]OptionData = null;
var node_name: []const u8 = "text";
var checked: ?bool = null;
var tag_name: []const u8 = "text";
if (node.is(Element)) |el| {
node_name = el.getTagNameLower();
tag_name = el.getTagNameLower();
if (el.is(Element.Html.Input)) |input| {
value = input.getValue();
if (el.getAttributeSafe(comptime lp.String.wrap("list"))) |list_id| {
if (input._input_type == .checkbox or input._input_type == .radio) {
checked = input.getChecked();
}
if (el.getAttributeSafe(comptime .wrap("list"))) |list_id| {
options = try extractDataListOptions(list_id, self.page, self.arena);
}
} else if (el.is(Element.Html.TextArea)) |textarea| {
@@ -172,8 +179,10 @@ fn walk(
is_interactive = true;
}
}
is_disabled = el.isDisabled();
} else if (node._type == .document or node._type == .document_fragment) {
node_name = "root";
tag_name = "root";
}
const initial_xpath_len = ctx.xpath_buffer.items.len;
@@ -234,9 +243,11 @@ fn walk(
.name = name,
.value = value,
.options = options,
.checked = checked,
.xpath = xpath,
.is_interactive = is_interactive,
.node_name = node_name,
.interactive = is_interactive,
.disabled = is_disabled,
.tag_name = tag_name,
};
if (should_visit) {
@@ -335,7 +346,7 @@ const JsonVisitor = struct {
try self.jw.write(data.id);
try self.jw.objectField("nodeName");
try self.jw.write(data.node_name);
try self.jw.write(data.tag_name);
try self.jw.objectField("xpath");
try self.jw.write(data.xpath);
@@ -345,7 +356,12 @@ const JsonVisitor = struct {
try self.jw.write(1);
try self.jw.objectField("isInteractive");
try self.jw.write(data.is_interactive);
try self.jw.write(data.interactive);
if (data.disabled) {
try self.jw.objectField("isDisabled");
try self.jw.write(true);
}
try self.jw.objectField("role");
try self.jw.write(data.role);
@@ -373,6 +389,11 @@ const JsonVisitor = struct {
try self.jw.endObject();
}
if (data.checked) |checked| {
try self.jw.objectField("checked");
try self.jw.write(checked);
}
if (data.options) |options| {
try self.jw.objectField("options");
try self.jw.beginArray();
@@ -459,6 +480,9 @@ const TextVisitor = struct {
const is_text_only = std.mem.eql(u8, data.role, "StaticText") or std.mem.eql(u8, data.role, "none") or std.mem.eql(u8, data.role, "generic");
try self.writer.print("{d}", .{data.id});
if (data.interactive) {
try self.writer.writeAll(if (data.disabled) " [i:disabled]" else " [i]");
}
if (!is_text_only) {
try self.writer.print(" {s}", .{data.role});
}
@@ -472,6 +496,14 @@ const TextVisitor = struct {
}
}
if (data.checked) |c| {
if (c) {
try self.writer.writeAll(" [checked]");
} else {
try self.writer.writeAll(" [unchecked]");
}
}
if (data.options) |options| {
try self.writer.writeAll(" options=[");
for (options, 0..) |opt, i| {
@@ -509,13 +541,189 @@ const TextVisitor = struct {
}
};
pub const NodeDetails = struct {
backendNodeId: CDPNode.Id,
tag_name: []const u8,
role: []const u8,
name: ?[]const u8,
interactive: bool,
disabled: bool,
value: ?[]const u8 = null,
input_type: ?[]const u8 = null,
placeholder: ?[]const u8 = null,
href: ?[]const u8 = null,
id: ?[]const u8 = null,
class: ?[]const u8 = null,
checked: ?bool = null,
options: ?[]OptionData = null,
pub fn jsonStringify(self: *const NodeDetails, jw: anytype) !void {
try jw.beginObject();
try jw.objectField("backendNodeId");
try jw.write(self.backendNodeId);
try jw.objectField("tagName");
try jw.write(self.tag_name);
try jw.objectField("role");
try jw.write(self.role);
if (self.name) |n| {
try jw.objectField("name");
try jw.write(n);
}
try jw.objectField("isInteractive");
try jw.write(self.interactive);
if (self.disabled) {
try jw.objectField("isDisabled");
try jw.write(true);
}
if (self.value) |v| {
try jw.objectField("value");
try jw.write(v);
}
if (self.input_type) |v| {
try jw.objectField("inputType");
try jw.write(v);
}
if (self.placeholder) |v| {
try jw.objectField("placeholder");
try jw.write(v);
}
if (self.href) |v| {
try jw.objectField("href");
try jw.write(v);
}
if (self.id) |v| {
try jw.objectField("id");
try jw.write(v);
}
if (self.class) |v| {
try jw.objectField("class");
try jw.write(v);
}
if (self.checked) |c| {
try jw.objectField("checked");
try jw.write(c);
}
if (self.options) |opts| {
try jw.objectField("options");
try jw.beginArray();
for (opts) |opt| {
try jw.beginObject();
try jw.objectField("value");
try jw.write(opt.value);
try jw.objectField("text");
try jw.write(opt.text);
if (opt.selected) {
try jw.objectField("selected");
try jw.write(true);
}
try jw.endObject();
}
try jw.endArray();
}
try jw.endObject();
}
};
pub fn getNodeDetails(
arena: std.mem.Allocator,
node: *Node,
registry: *CDPNode.Registry,
page: *Page,
) !NodeDetails {
const cdp_node = try registry.register(node);
const axn = AXNode.fromNode(node);
const role = try axn.getRole();
const name = try axn.getName(page, arena);
var is_interactive = false;
var is_disabled = false;
var tag_name: []const u8 = "text";
var value: ?[]const u8 = null;
var input_type: ?[]const u8 = null;
var placeholder: ?[]const u8 = null;
var href: ?[]const u8 = null;
var id_attr: ?[]const u8 = null;
var class_attr: ?[]const u8 = null;
var checked: ?bool = null;
var options: ?[]OptionData = null;
if (node.is(Element)) |el| {
tag_name = el.getTagNameLower();
is_disabled = el.isDisabled();
id_attr = el.getAttributeSafe(comptime .wrap("id"));
class_attr = el.getAttributeSafe(comptime .wrap("class"));
placeholder = el.getAttributeSafe(comptime .wrap("placeholder"));
if (el.getAttributeSafe(comptime .wrap("href"))) |h| {
const URL = lp.URL;
href = URL.resolve(arena, page.base(), h, .{ .encode = true }) catch h;
}
if (el.is(Element.Html.Input)) |input| {
value = input.getValue();
input_type = input._input_type.toString();
if (input._input_type == .checkbox or input._input_type == .radio) {
checked = input.getChecked();
}
if (el.getAttributeSafe(comptime .wrap("list"))) |list_id| {
options = try extractDataListOptions(list_id, page, arena);
}
} else if (el.is(Element.Html.TextArea)) |textarea| {
value = textarea.getValue();
} else if (el.is(Element.Html.Select)) |select| {
value = select.getValue(page);
options = try extractSelectOptions(el.asNode(), page, arena);
}
if (el.is(Element.Html)) |html_el| {
const listener_targets = try interactive.buildListenerTargetMap(page, arena);
var pointer_events_cache: Element.PointerEventsCache = .empty;
if (interactive.classifyInteractivity(page, el, html_el, listener_targets, &pointer_events_cache) != null) {
is_interactive = true;
}
}
}
return .{
.backendNodeId = cdp_node.id,
.tag_name = tag_name,
.role = role,
.name = name,
.interactive = is_interactive,
.disabled = is_disabled,
.value = value,
.input_type = input_type,
.placeholder = placeholder,
.href = href,
.id = id_attr,
.class = class_attr,
.checked = checked,
.options = options,
};
}
const testing = @import("testing.zig");
test "SemanticTree backendDOMNodeId" {
var registry: CDPNode.Registry = .init(testing.allocator);
defer registry.deinit();
var page = try testing.pageTest("cdp/registry1.html");
var page = try testing.pageTest("cdp/registry1.html", .{});
defer testing.reset();
defer page._session.removePage();
@@ -539,7 +747,7 @@ test "SemanticTree max_depth" {
var registry: CDPNode.Registry = .init(testing.allocator);
defer registry.deinit();
var page = try testing.pageTest("cdp/registry1.html");
var page = try testing.pageTest("cdp/registry1.html", .{});
defer testing.reset();
defer page._session.removePage();

View File

@@ -205,7 +205,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void {
event.acquireRef();
defer event.deinit(false, self.page._session);
defer _ = event.releaseRef(self.page._session);
if (comptime IS_DEBUG) {
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
@@ -240,7 +240,7 @@ pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event,
defer window._current_event = prev_event;
event.acquireRef();
defer event.deinit(false, page._session);
defer _ = event.releaseRef(page._session);
if (comptime IS_DEBUG) {
log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context });

View File

@@ -239,7 +239,7 @@ fn eventInit(arena: Allocator, typ: String, value: anytype) !Event {
const time_stamp = (raw_timestamp / 2) * 2;
return .{
._rc = 0,
._rc = .{},
._arena = arena,
._type = unionInit(Event.Type, value),
._type_string = typ,
@@ -255,6 +255,7 @@ pub fn blob(_: *const Factory, arena: Allocator, child: anytype) !*@TypeOf(child
const blob_ptr = chain.get(0);
blob_ptr.* = .{
._rc = .{},
._arena = arena,
._type = unionInit(Blob.Type, chain.get(1)),
._slice = "",
@@ -271,7 +272,7 @@ pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, page:
const doc = page.document.asNode();
const abstract_range = chain.get(0);
abstract_range.* = AbstractRange{
._rc = 0,
._rc = .{},
._arena = arena,
._page_id = page.id,
._type = unionInit(AbstractRange.Type, chain.get(1)),

View File

@@ -30,7 +30,7 @@ const Notification = @import("../Notification.zig");
const CookieJar = @import("webapi/storage/Cookie.zig").Jar;
const http = @import("../network/http.zig");
const Runtime = @import("../network/Runtime.zig");
const Network = @import("../network/Network.zig");
const Robots = @import("../network/Robots.zig");
const IS_DEBUG = builtin.mode == .Debug;
@@ -86,7 +86,7 @@ queue: std.DoublyLinkedList = .{},
// The main app allocator
allocator: Allocator,
network: *Runtime,
network: *Network,
// Queue of requests that depend on a robots.txt.
// Allows us to fetch the robots.txt just once.
@@ -131,7 +131,7 @@ pub const CDPClient = struct {
blocking_read_end: *const fn (*anyopaque) bool,
};
pub fn init(allocator: Allocator, network: *Runtime) !*Client {
pub fn init(allocator: Allocator, network: *Network) !*Client {
var transfer_pool = std.heap.MemoryPool(Transfer).init(allocator);
errdefer transfer_pool.deinit();
@@ -695,7 +695,7 @@ fn perform(self: *Client, timeout_ms: c_int) anyerror!PerformStatus {
break :blk try self.handles.perform();
};
// Process dirty connections — return them to Runtime pool.
// Process dirty connections — return them to Network pool.
while (self.dirty.popFirst()) |node| {
const conn: *http.Connection = @fieldParentPtr("node", node);
self.handles.remove(conn) catch |err| {
@@ -821,16 +821,16 @@ fn processOneMessage(self: *Client, msg: http.Handles.MultiMessage, transfer: *T
break :blk std.ascii.eqlIgnoreCase(hdr.value, "close");
};
if (msg.err != null and !is_conn_close_recv) {
transfer.requestFailed(transfer._callback_error orelse msg.err.?, true);
return true;
}
// make sure the transfer can't be immediately aborted from a callback
// since we still need it here.
transfer._performing = true;
defer transfer._performing = false;
if (msg.err != null and !is_conn_close_recv) {
transfer.requestFailed(transfer._callback_error orelse msg.err.?, true);
return true;
}
if (!transfer._header_done_called) {
// In case of request w/o data, we need to call the header done
// callback now.
@@ -873,7 +873,6 @@ fn processMessages(self: *Client) !bool {
var processed = false;
while (self.handles.readMessage()) |msg| {
const transfer = try Transfer.fromConnection(&msg.conn);
const done = self.processOneMessage(msg, transfer) catch |err| blk: {
log.err(.http, "process_messages", .{ .err = err, .req = transfer });
transfer.requestFailed(err, true);
@@ -1068,6 +1067,24 @@ pub const Transfer = struct {
if (self.req.shutdown_callback) |cb| {
cb(self.ctx);
}
if (self._performing or self.client.performing) {
// We're currently inside of a callback. This client, and libcurl
// generally don't expect a transfer to become deinitialized during
// a callback. We can flag the transfer as aborted (which is what
// we do when transfer.abort() is called in this condition) AND,
// since this "kill()"should prevent any future callbacks, the best
// we can do is null/noop them.
self.aborted = true;
self.req.start_callback = null;
self.req.shutdown_callback = null;
self.req.header_callback = Noop.headerCallback;
self.req.data_callback = Noop.dataCallback;
self.req.done_callback = Noop.doneCallback;
self.req.error_callback = Noop.errorCallback;
return;
}
self.deinit();
}
@@ -1244,15 +1261,21 @@ pub const Transfer = struct {
fn detectAuthChallenge(transfer: *Transfer, conn: *const http.Connection) void {
const status = conn.getResponseCode() catch return;
if (status != 401 and status != 407) {
const connect_status = conn.getConnectCode() catch return;
if (status != 401 and status != 407 and connect_status != 401 and connect_status != 407) {
transfer._auth_challenge = null;
return;
}
if (conn.getResponseHeader("WWW-Authenticate", 0)) |hdr| {
transfer._auth_challenge = http.AuthChallenge.parse(status, .server, hdr.value) catch null;
} else if (conn.getConnectHeader("WWW-Authenticate", 0)) |hdr| {
transfer._auth_challenge = http.AuthChallenge.parse(status, .server, hdr.value) catch null;
} else if (conn.getResponseHeader("Proxy-Authenticate", 0)) |hdr| {
transfer._auth_challenge = http.AuthChallenge.parse(status, .proxy, hdr.value) catch null;
} else if (conn.getConnectHeader("Proxy-Authenticate", 0)) |hdr| {
transfer._auth_challenge = http.AuthChallenge.parse(status, .proxy, hdr.value) catch null;
} else {
transfer._auth_challenge = .{ .status = status, .source = null, .scheme = null, .realm = null };
}
@@ -1325,15 +1348,15 @@ pub const Transfer = struct {
}
}
transfer.req.notification.dispatch(.http_response_header_done, &.{
.transfer = transfer,
});
const proceed = transfer.req.header_callback(transfer) catch |err| {
log.err(.http, "header_callback", .{ .err = err, .req = transfer });
return err;
};
transfer.req.notification.dispatch(.http_response_header_done, &.{
.transfer = transfer,
});
return proceed and transfer.aborted == false;
}
@@ -1492,3 +1515,12 @@ pub const Transfer = struct {
return null;
}
};
const Noop = struct {
fn headerCallback(_: *Transfer) !bool {
return true;
}
fn dataCallback(_: *Transfer, _: []const u8) !void {}
fn doneCallback(_: *anyopaque) !void {}
fn errorCallback(_: *anyopaque, _: anyerror) void {}
};

View File

@@ -487,7 +487,6 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
return error.InjectBlankFailed;
};
}
self.documentIsComplete();
session.notification.dispatch(.page_navigate, &.{
.frame_id = self._frame_id,
@@ -519,6 +518,8 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
// force next request id manually b/c we won't create a real req.
_ = session.browser.http_client.incrReqId();
self.documentIsComplete();
return;
}
@@ -738,6 +739,12 @@ pub fn _documentIsLoaded(self: *Page) !void {
self.document.asEventTarget(),
event,
);
self._session.notification.dispatch(.page_dom_content_loaded, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.timestamp = timestamp(.monotonic),
});
}
pub fn scriptsCompletedLoading(self: *Page) void {
@@ -796,19 +803,6 @@ pub fn documentIsComplete(self: *Page) void {
self._documentIsComplete() catch |err| {
log.err(.page, "document is complete", .{ .err = err, .type = self._type, .url = self.url });
};
if (self._navigated_options) |no| {
// _navigated_options will be null in special short-circuit cases, like
// "navigating" to about:blank, in which case this notification has
// already been sent
self._session.notification.dispatch(.page_navigated, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.opts = no,
.url = self.url,
.timestamp = timestamp(.monotonic),
});
}
}
fn _documentIsComplete(self: *Page) !void {
@@ -827,6 +821,12 @@ fn _documentIsComplete(self: *Page) !void {
try self._event_manager.dispatchDirect(window_target, event, self.window._on_load, .{ .inject_target = false, .context = "page load" });
}
self._session.notification.dispatch(.page_loaded, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.timestamp = timestamp(.monotonic),
});
if (self._event_manager.hasDirectListeners(window_target, "pageshow", self.window._on_pageshow)) {
const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent();
try self._event_manager.dispatchDirect(window_target, pageshow_event, self.window._on_pageshow, .{ .context = "page show" });
@@ -879,6 +879,19 @@ fn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
});
}
if (self._navigated_options) |no| {
// _navigated_options will be null in special short-circuit cases, like
// "navigating" to about:blank, in which case this notification has
// already been sent
self._session.notification.dispatch(.page_navigated, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.opts = no,
.url = self.url,
.timestamp = timestamp(.monotonic),
});
}
return true;
}
@@ -3392,7 +3405,7 @@ pub fn handleClick(self: *Page, target: *Node) !void {
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
const event = keyboard_event.asEvent();
const element = self.window._document._active_element orelse {
keyboard_event.deinit(false, self._session);
_ = event.releaseRef(self._session);
return;
};
@@ -3488,7 +3501,7 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
// so submit_event is still valid when we check _prevent_default
submit_event.acquireRef();
defer submit_event.deinit(false, self._session);
defer _ = submit_event.releaseRef(self._session);
try self._event_manager.dispatch(form_element.asEventTarget(), submit_event);
// If the submit event was prevented, don't submit the form

View File

@@ -22,10 +22,14 @@ const builtin = @import("builtin");
const log = @import("../log.zig");
const js = @import("js/js.zig");
const Page = @import("Page.zig");
const Session = @import("Session.zig");
const HttpClient = @import("HttpClient.zig");
const Node = @import("webapi/Node.zig");
const Selector = @import("webapi/selector/Selector.zig");
const IS_DEBUG = builtin.mode == .Debug;
const Runner = @This();
@@ -236,3 +240,111 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
.raw_done => return .done,
}
}
pub fn waitForSelector(self: *Runner, selector: [:0]const u8, timeout_ms: u32) !*Node.Element {
const arena = try self.session.getArena(.{ .debug = "Runner.waitForSelector" });
defer self.session.releaseArena(arena);
var timer = try std.time.Timer.start();
const parsed_selector = try Selector.parseLeaky(arena, selector);
while (true) {
// self.page can change between ticks
const page = self.page;
if (try parsed_selector.query(page.document.asNode(), page)) |el| {
return el;
}
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
if (elapsed >= timeout_ms) {
return error.Timeout;
}
switch (try self.tick(.{ .ms = timeout_ms - elapsed })) {
.done => return error.Timeout,
.ok => |recommended_sleep_ms| {
if (recommended_sleep_ms > 0) {
std.Thread.sleep(std.time.ns_per_ms * recommended_sleep_ms);
}
},
}
}
}
pub fn waitForScript(runner: *Runner, script: [:0]const u8, timeout_ms: u32) !void {
var timer = try std.time.Timer.start();
while (true) {
const page = runner.page;
// Execute the script and check if it returns truthy
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
var try_catch: js.TryCatch = undefined;
try_catch.init(&ls.local);
defer try_catch.deinit();
const value = ls.local.exec(script, "wait_script") catch |err| {
const caught = try_catch.caughtOrError(page.call_arena, err);
log.err(.app, "wait script error", .{ .err = caught });
return error.ScriptError;
};
if (value.toBool()) {
return;
}
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
if (elapsed >= timeout_ms) {
return error.Timeout;
}
switch (try runner.tick(.{ .ms = timeout_ms - elapsed })) {
.done => return error.Timeout,
.ok => |recommended_sleep_ms| {
if (recommended_sleep_ms > 0) {
std.Thread.sleep(std.time.ns_per_ms * recommended_sleep_ms);
}
},
}
}
}
const testing = @import("../testing.zig");
test "Runner: no page" {
try testing.expectError(error.NoPage, Runner.init(testing.test_session, .{}));
}
test "Runner: waitForSelector timeout" {
const page = try testing.pageTest("runner/runner1.html", .{});
defer page._session.removePage();
var runner = try page._session.runner(.{});
try testing.expectError(error.Timeout, runner.waitForSelector("#nope", 10));
}
test "Runner: waitForSelector" {
defer testing.reset();
const page = try testing.pageTest("runner/runner1.html", .{});
defer page._session.removePage();
var runner = try page._session.runner(.{});
const el = try runner.waitForSelector("#sel1", 10);
try testing.expectEqual("selector-1-content", try el.asNode().getTextContentAlloc(testing.arena_allocator));
}
test "Runner: waitForScript timeout" {
const page = try testing.pageTest("runner/runner1.html", .{});
defer page._session.removePage();
var runner = try page._session.runner(.{});
try testing.expectError(error.Timeout, runner.waitForScript("document.querySelector('#nope')", 10));
}
test "Runner: waitForScript" {
const page = try testing.pageTest("runner/runner1.html", .{});
defer page._session.removePage();
var runner = try page._session.runner(.{});
try runner.waitForScript("document.querySelector('#sel1')", 10);
}

View File

@@ -22,7 +22,7 @@ const builtin = @import("builtin");
const log = @import("../log.zig");
const HttpClient = @import("HttpClient.zig");
const net_http = @import("../network/http.zig");
const http = @import("../network/http.zig");
const String = @import("../string.zig").String;
const js = @import("js/js.zig");
@@ -136,7 +136,7 @@ fn clearList(list: *std.DoublyLinkedList) void {
}
}
fn getHeaders(self: *ScriptManager) !net_http.Headers {
fn getHeaders(self: *ScriptManager) !http.Headers {
var headers = try self.client.newHeaders();
try self.page.headersForRequest(&headers);
return headers;

View File

@@ -71,6 +71,18 @@ origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
// ensuring object identity works across same-origin frames.
identity: js.Identity = .{},
// Shared finalizer callbacks across all Identities. Keyed by Zig instance ptr.
// This ensures objects are only freed when ALL v8 wrappers are gone.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
// Tracked global v8 objects that need to be released on cleanup.
// Lives at Session level so objects can outlive individual Identities.
globals: std.ArrayList(v8.Global) = .empty,
// Temporary v8 globals that can be released early. Key is global.data_ptr.
// Lives at Session level so objects holding Temps can outlive individual Identities.
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Shared resources for all pages in this session.
// These live for the duration of the page tree (root + frames).
arena_pool: *ArenaPool,
@@ -224,6 +236,30 @@ pub fn releaseOrigin(self: *Session, origin: *js.Origin) void {
/// Reset page_arena and factory for a clean slate.
/// Called when root page is removed.
fn resetPageResources(self: *Session) void {
// Force cleanup all remaining finalized objects
{
var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |fc| {
fc.*.deinit(self);
}
self.finalizer_callbacks = .empty;
}
{
for (self.globals.items) |*global| {
v8.v8__Global__Reset(global);
}
self.globals = .empty;
}
{
var it = self.temps.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
self.temps = .empty;
}
self.identity.deinit();
self.identity = .{};
@@ -457,35 +493,25 @@ pub fn nextPageId(self: *Session) u32 {
return id;
}
// A type that has a finalizer can have its finalizer called one of two ways.
// The first is from V8 via the WeakCallback we give to weakRef. But that isn't
// guaranteed to fire, so we track this in finalizer_callbacks and call them on
// page reset.
// Every finalizable instance of Zig gets 1 FinalizerCallback registered in the
// session. This is to ensure that, if v8 doesn't finalize the value, we can
// release on page reset.
pub const FinalizerCallback = struct {
arena: Allocator,
session: *Session,
ptr: *anyopaque,
global: v8.Global,
identity: *js.Identity,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
resolved_ptr_id: usize,
finalizer_ptr_id: usize,
_deinit: *const fn (ptr_id: usize, session: *Session) void,
pub fn deinit(self: *FinalizerCallback) void {
self.zig_finalizer(self.ptr, self.session);
self.session.releaseArena(self.arena);
}
/// Release this item from the identity tracking maps (called after finalizer runs from V8)
pub fn releaseIdentity(self: *FinalizerCallback) void {
const session = self.session;
const id = @intFromPtr(self.ptr);
if (self.identity.identity_map.fetchRemove(id)) |kv| {
var global = kv.value;
v8.v8__Global__Reset(&global);
}
_ = self.identity.finalizer_callbacks.remove(id);
// For every FinalizerCallback we'll have 1+ FinalizerCallback.Identity: one
// for every identity that gets the instance. In most cases, that'l be 1.
pub const Identity = struct {
identity: *js.Identity,
fc: *Session.FinalizerCallback,
};
fn deinit(self: *FinalizerCallback, session: *Session) void {
self._deinit(self.finalizer_ptr_id, session);
session.releaseArena(self.arena);
}
};

View File

@@ -114,7 +114,7 @@ fn addRawRule(self: *StyleManager, selector_text: []const u8, block_text: []cons
if (!props.isRelevant()) return;
const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return;
const selectors = SelectorParser.parseList(self.arena, selector_text) catch return;
for (selectors) |selector| {
const rightmost = if (selector.segments.len > 0) selector.segments[selector.segments.len - 1].compound else selector.first;
const bucket_key = getBucketKey(rightmost) orelse continue;
@@ -484,7 +484,7 @@ fn addRule(self: *StyleManager, style_rule: *CSSStyleRule) !void {
}
// Parse the selector list
const selectors = SelectorParser.parseList(self.arena, selector_text, self.page) catch return;
const selectors = SelectorParser.parseList(self.arena, selector_text) catch return;
if (selectors.len == 0) {
return;
}

View File

@@ -25,28 +25,72 @@ const ResolveOpts = struct {
};
// path is anytype, so that it can be used with both []const u8 and [:0]const u8
pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime opts: ResolveOpts) ![:0]const u8 {
const PT = @TypeOf(path);
if (base.len == 0 or isCompleteHTTPUrl(path)) {
if (comptime opts.always_dupe or !isNullTerminated(PT)) {
const duped = try allocator.dupeZ(u8, path);
return processResolved(allocator, duped, opts);
pub fn resolve(allocator: Allocator, base: [:0]const u8, source_path: anytype, comptime opts: ResolveOpts) ![:0]const u8 {
const PT = @TypeOf(source_path);
var path: [:0]const u8 = if (comptime !isNullTerminated(PT) or opts.always_dupe) try allocator.dupeZ(u8, source_path) else source_path;
if (base.len == 0) {
return processResolved(allocator, path, opts);
}
// Minimum is "x:" and skip relative path (very common case)
if (path.len >= 2 and path[0] != '/') {
if (std.mem.indexOfScalar(u8, path[0..], ':')) |scheme_path_end| {
scheme_check: {
const scheme_path = path[0..scheme_path_end];
//from "ws" to "https"
if (scheme_path_end >= 2 and scheme_path_end <= 5) {
const has_double_slashes: bool = scheme_path_end + 3 <= path.len and path[scheme_path_end + 1] == '/' and path[scheme_path_end + 2] == '/';
const special_schemes = [_][]const u8{ "https", "http", "ws", "wss", "file", "ftp" };
for (special_schemes) |special_scheme| {
if (std.ascii.eqlIgnoreCase(scheme_path, special_scheme)) {
const base_scheme_end = std.mem.indexOf(u8, base, "://") orelse 0;
if (base_scheme_end > 0 and std.mem.eql(u8, base[0..base_scheme_end], scheme_path) and !has_double_slashes) {
//Skip ":" and exit as relative state
path = path[scheme_path_end + 1 ..];
break :scheme_check;
} else {
var rest_start: usize = scheme_path_end + 1;
//Skip any slashas after "scheme:"
while (rest_start < path.len and (path[rest_start] == '/' or path[rest_start] == '\\')) {
rest_start += 1;
}
// A special scheme (exclude "file") must contain at least any chars after "://"
if (rest_start == path.len and !std.ascii.eqlIgnoreCase(scheme_path, "file")) {
return error.TypeError;
}
//File scheme allow empty host
const separator: []const u8 = if (!has_double_slashes and std.ascii.eqlIgnoreCase(scheme_path, "file")) ":///" else "://";
path = try std.mem.joinZ(allocator, "", &.{ scheme_path, separator, path[rest_start..] });
return processResolved(allocator, path, opts);
}
}
}
}
if (scheme_path.len > 0) {
for (scheme_path[1..]) |c| {
if (!std.ascii.isAlphanumeric(c) and c != '+' and c != '-' and c != '.') {
//Exit as relative state
break :scheme_check;
}
}
}
//path is complete http url
return processResolved(allocator, path, opts);
}
}
if (comptime opts.encode) {
return processResolved(allocator, path, opts);
}
return path;
}
if (path.len == 0) {
if (comptime opts.always_dupe) {
const duped = try allocator.dupeZ(u8, base);
return processResolved(allocator, duped, opts);
if (opts.always_dupe) {
const dupe = try allocator.dupeZ(u8, base);
return processResolved(allocator, dupe, opts);
}
if (comptime opts.encode) {
return processResolved(allocator, base, opts);
}
return base;
return processResolved(allocator, base, opts);
}
if (path[0] == '?') {
@@ -63,55 +107,13 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
if (std.mem.startsWith(u8, path, "//")) {
// network-path reference
const index = std.mem.indexOfScalar(u8, base, ':') orelse {
if (comptime isNullTerminated(PT)) {
if (comptime opts.encode) {
return processResolved(allocator, path, opts);
}
return path;
}
const duped = try allocator.dupeZ(u8, path);
return processResolved(allocator, duped, opts);
return processResolved(allocator, path, opts);
};
const protocol = base[0 .. index + 1];
const result = try std.mem.joinZ(allocator, "", &.{ protocol, path });
return processResolved(allocator, result, opts);
}
if (path.len >= 4) { // Minimum: "ws:x"
if (std.mem.indexOfScalar(u8, path[0..@min(path.len, 6)], ':')) |pos| {
// we know this isn't a complete URL, else the very first check in
// this function would have handled it.
const possible_special_protocol = path[0..pos];
const special_schemes = [_][]const u8{ "https", "http", "ws", "wss", "file", "ftp" };
for (special_schemes) |special_scheme| {
if (std.ascii.eqlIgnoreCase(possible_special_protocol, special_scheme)) {
const rest = path[pos + 1 ..];
// Check if base has the same scheme
const base_scheme_end = std.mem.indexOf(u8, base, "://") orelse 0;
if (base_scheme_end > 0 and std.ascii.eqlIgnoreCase(base[0..base_scheme_end], special_scheme)) {
// Same scheme - strip it and resolve rest as relative
return resolve(allocator, base, rest, opts);
}
// Different scheme - construct absolute URL
// Skip any leading slashes in rest
var rest_start: usize = 0;
while (rest_start < rest.len and (rest[rest_start] == '/' or rest[rest_start] == '\\')) {
rest_start += 1;
}
const rest_trimmed = rest[rest_start..];
// file: scheme needs empty host (triple slash)
const separator = if (std.mem.eql(u8, special_scheme, "file")) ":///" else "://";
const normalized = try std.mem.joinZ(allocator, "", &.{ special_scheme, separator, rest_trimmed });
return resolve(allocator, "", normalized, opts);
}
}
// Don't know what this is, just try to resolve it through our normal logic
}
}
const scheme_end = std.mem.indexOf(u8, base, "://");
const authority_start = if (scheme_end) |end| end + 3 else 0;
const path_start = std.mem.indexOfScalarPos(u8, base, authority_start, '/') orelse base.len;
@@ -131,6 +133,7 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
// trailing space so that we always have space to append the null terminator
// and so that we can compare the next two characters without needing to length check
var out = try std.mem.join(allocator, "", &.{ normalized_base, "/", path, " " });
const end = out.len - 2;
const path_marker = path_start + 1;
@@ -1611,78 +1614,176 @@ test "URL: resolve path scheme" {
base: [:0]const u8,
path: [:0]const u8,
expected: [:0]const u8,
expected_error: bool = false,
};
const cases = [_]Case{
//same schemes and path as relative path (one slash)
.{
.base = "https://www.example.com/example",
.path = "https:/about",
.expected = "https://www.example.com/about",
},
//same schemes and path as relative path (without slash)
.{
.base = "https://www.example.com/example",
.path = "https:about",
.expected = "https://www.example.com/about",
},
//same schemes and path as absolute path (two slashes)
.{
.base = "https://www.example.com/example",
.path = "https://about",
.expected = "https://about",
},
//different schemes and path as absolute (without slash)
.{
.base = "https://www.example.com/example",
.path = "http:about",
.expected = "http://about",
},
//different schemes and path as absolute (with one slash)
.{
.base = "https://www.example.com/example",
.path = "http:/about",
.expected = "http://about",
},
//different schemes and path as absolute (with two slashes)
.{
.base = "https://www.example.com/example",
.path = "http://about",
.expected = "http://about",
},
//same schemes and path as absolute (with more slashes)
.{
.base = "https://site/",
.path = "https://path",
.expected = "https://path",
},
//path scheme is not special and path as absolute (without additional slashes)
.{
.base = "http://localhost/",
.path = "data:test",
.expected = "data:test",
},
//different schemes and path as absolute (pathscheme=ws)
.{
.base = "https://www.example.com/example",
.path = "ws://about",
.expected = "ws://about",
},
//different schemes and path as absolute (path scheme=wss)
.{
.base = "https://www.example.com/example",
.path = "wss://about",
.expected = "wss://about",
},
//different schemes and path as absolute (path scheme=ftp)
.{
.base = "https://www.example.com/example",
.path = "ftp://about",
.expected = "ftp://about",
},
//different schemes and path as absolute (path scheme=file)
.{
.base = "https://www.example.com/example",
.path = "file://path/to/file",
.expected = "file://path/to/file",
},
//different schemes and path as absolute (path scheme=file, host is empty)
.{
.base = "https://www.example.com/example",
.path = "file:/path/to/file",
.expected = "file:///path/to/file",
},
//different schemes and path as absolute (path scheme=file, host is empty)
.{
.base = "https://www.example.com/example",
.path = "file:/",
.expected = "file:///",
},
//different schemes without :// and normalize "file" scheme, absolute path
.{
.base = "https://www.example.com/example",
.path = "file:path/to/file",
.expected = "file:///path/to/file",
},
//same schemes without :// in path and rest starts with scheme:/, relative path
.{
.base = "https://www.example.com/example",
.path = "https:/file:/relative/path/",
.expected = "https://www.example.com/file:/relative/path/",
},
//same schemes without :// in path and rest starts with scheme://, relative path
.{
.base = "https://www.example.com/example",
.path = "https:/http://relative/path/",
.expected = "https://www.example.com/http://relative/path/",
},
//same schemes without :// in path , relative state
.{
.base = "http://www.example.com/example",
.path = "http:relative:path",
.expected = "http://www.example.com/relative:path",
},
//repeat different schemes in path
.{
.base = "http://www.example.com/example",
.path = "http:http:/relative/path/",
.expected = "http://www.example.com/http:/relative/path/",
},
//repeat different schemes in path
.{
.base = "http://www.example.com/example",
.path = "http:https://relative:path",
.expected = "http://www.example.com/https://relative:path",
},
//NOT required :// for blob scheme
.{
.base = "http://www.example.com/example",
.path = "blob:other",
.expected = "blob:other",
},
//NOT required :// for NON-special schemes and can contains "+" or "-" or "." in scheme
.{
.base = "http://www.example.com/example",
.path = "custom+foo:other",
.expected = "custom+foo:other",
},
//NOT required :// for NON-special schemes
.{
.base = "http://www.example.com/example",
.path = "blob:",
.expected = "blob:",
},
//NOT required :// for special scheme equal base scheme
.{
.base = "http://www.example.com/example",
.path = "http:",
.expected = "http://www.example.com/example",
},
//required :// for special scheme, so throw error.InvalidURL
.{
.base = "http://www.example.com/example",
.path = "https:",
.expected = "",
.expected_error = true,
},
//incorrect symbols in path scheme
.{
.base = "https://site",
.path = "http?://host/some",
.expected = "https://site/http?://host/some",
},
};
for (cases) |case| {
const result = try resolve(testing.arena_allocator, case.base, case.path, .{});
try testing.expectString(case.expected, result);
if (case.expected_error) {
const result = resolve(testing.arena_allocator, case.base, case.path, .{});
try testing.expectError(error.TypeError, result);
} else {
const result = try resolve(testing.arena_allocator, case.base, case.path, .{});
try testing.expectString(case.expected, result);
}
}
}

View File

@@ -110,28 +110,10 @@ pub fn waitForSelector(selector: [:0]const u8, timeout_ms: u32, session: *Sessio
var runner = try session.runner(.{});
try runner.wait(.{ .ms = timeout_ms, .until = .load });
while (true) {
const page = runner.page;
const element = Selector.querySelector(page.document.asNode(), selector, page) catch {
return error.InvalidSelector;
};
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
const remaining = timeout_ms -| elapsed;
if (remaining == 0) return error.Timeout;
if (element) |el| {
return el.asNode();
}
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
if (elapsed >= timeout_ms) {
return error.Timeout;
}
switch (try runner.tick(.{ .ms = timeout_ms - elapsed })) {
.done => return error.Timeout,
.ok => |recommended_sleep_ms| {
if (recommended_sleep_ms > 0) {
// guanrateed to be <= 20ms
std.Thread.sleep(std.time.ns_per_ms * recommended_sleep_ms);
}
},
}
}
const el = try runner.waitForSelector(selector, remaining);
return el.asNode();
}

View File

@@ -21,6 +21,7 @@ const lp = @import("lightpanda");
const log = @import("../../log.zig");
const js = @import("js.zig");
const bridge = @import("bridge.zig");
const Env = @import("Env.zig");
const Origin = @import("Origin.zig");
const Scheduler = @import("Scheduler.zig");
@@ -213,48 +214,11 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
}
pub fn trackGlobal(self: *Context, global: v8.Global) !void {
return self.identity.globals.append(self.identity_arena, global);
return self.session.globals.append(self.session.page_arena, global);
}
pub fn trackTemp(self: *Context, global: v8.Global) !void {
return self.identity.temps.put(self.identity_arena, global.data_ptr, global);
}
pub fn weakRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj);
const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
}
pub fn safeWeakRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj);
const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__ClearWeak(&fc.global);
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
}
pub fn strongRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj);
const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__ClearWeak(&fc.global);
return self.session.temps.put(self.session.page_arena, global.data_ptr, global);
}
pub const IdentityResult = struct {
@@ -270,35 +234,6 @@ pub fn addIdentity(self: *Context, ptr: usize) !IdentityResult {
};
}
pub fn releaseTemp(self: *Context, global: v8.Global) void {
if (self.identity.temps.fetchRemove(global.data_ptr)) |kv| {
var g = kv.value;
v8.v8__Global__Reset(&g);
}
}
pub fn createFinalizerCallback(
self: *Context,
global: v8.Global,
ptr: *anyopaque,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
) !*Session.FinalizerCallback {
const session = self.session;
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
errdefer session.releaseArena(arena);
const fc = try arena.create(Session.FinalizerCallback);
fc.* = .{
.arena = arena,
.session = session,
.ptr = ptr,
.global = global,
.zig_finalizer = zig_finalizer,
// Store identity pointer for cleanup when V8 GCs the object
.identity = self.identity,
};
return fc;
}
// Any operation on the context have to be made from a local.
pub fn localScope(self: *Context, ls: *js.Local.Scope) void {
const isolate = self.isolate;

View File

@@ -213,7 +213,7 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl
return .{ .handle = global, .temps = {} };
}
try ctx.trackTemp(global);
return .{ .handle = global, .temps = &ctx.identity.temps };
return .{ .handle = global, .temps = &ctx.session.temps };
}
pub fn tempWithThis(self: *const Function, value: anytype) !Temp {

View File

@@ -38,38 +38,9 @@ const Identity = @This();
// Maps Zig instance pointers to their v8::Global(Object) wrappers.
identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Tracked global v8 objects that need to be released on cleanup.
globals: std.ArrayList(v8.Global) = .empty,
// Temporary v8 globals that can be released early. Key is global.data_ptr.
temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Finalizer callbacks for weak references. Key is @intFromPtr of the Zig instance.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *Session.FinalizerCallback) = .empty,
pub fn deinit(self: *Identity) void {
{
var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |finalizer| {
finalizer.*.deinit();
}
}
{
var it = self.identity_map.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
for (self.globals.items) |*global| {
var it = self.identity_map.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
{
var it = self.temps.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
}

View File

@@ -17,10 +17,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Session = @import("../Session.zig");
const log = @import("../../log.zig");
const string = @import("../../string.zig");
const Session = @import("../Session.zig");
const js = @import("js.zig");
const bridge = @import("bridge.zig");
const Caller = @import("Caller.zig");
@@ -213,7 +214,8 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
.pointer => |ptr| {
const resolved = resolveValue(value);
const gop = try ctx.addIdentity(@intFromPtr(resolved.ptr));
const resolved_ptr_id = @intFromPtr(resolved.ptr);
const gop = try ctx.addIdentity(resolved_ptr_id);
if (gop.found_existing) {
// we've seen this instance before, return the same object
return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);
@@ -262,31 +264,27 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
// dont' use js_obj.persist(), because we don't want to track this in
// context.global_objects, we want to track it in context.identity_map.
v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr);
if (@hasDecl(JsApi.Meta, "finalizer")) {
// It would be great if resolved knew the resolved type, but I
// can't figure out how to make that work, since it depends on
// the [runtime] `value`.
// We need the resolved finalizer, which we have in resolved.
//
// The above if statement would be more clear as:
// if (resolved.finalizer_from_v8) |finalizer| {
// But that's a runtime check.
// Instead, we check if the base has finalizer. The assumption
// here is that if a resolve type has a finalizer, then the base
// should have a finalizer too.
const fc = try ctx.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);
{
errdefer fc.deinit();
try ctx.identity.finalizer_callbacks.put(ctx.identity_arena, @intFromPtr(resolved.ptr), fc);
}
if (resolved.finalizer) |finalizer| {
const finalizer_ptr_id = finalizer.ptr_id;
finalizer.acquireRef(finalizer_ptr_id);
conditionallyReference(value);
if (@hasDecl(JsApi.Meta, "weak")) {
if (comptime IS_DEBUG) {
std.debug.assert(JsApi.Meta.weak == true);
}
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, fc, resolved.finalizer_from_v8, v8.kParameter);
const session = ctx.session;
const finalizer_gop = try session.finalizer_callbacks.getOrPut(session.page_arena, finalizer_ptr_id);
if (finalizer_gop.found_existing == false) {
// This is the first context (and very likely only one) to
// see this Zig instance. We need to create the FinalizerCallback
// so that we can cleanup on page reset if v8 doesn't finalize.
errdefer _ = session.finalizer_callbacks.remove(finalizer_ptr_id);
finalizer_gop.value_ptr.* = try self.createFinalizerCallback(resolved_ptr_id, finalizer_ptr_id, finalizer.deinit);
}
const fc = finalizer_gop.value_ptr.*;
const identity_finalizer = try fc.arena.create(Session.FinalizerCallback.Identity);
identity_finalizer.* = .{
.fc = fc,
.identity = ctx.identity,
};
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, identity_finalizer, finalizer.release, v8.kParameter);
}
return js_obj;
},
@@ -1121,12 +1119,19 @@ fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T {
// This function recursively walks the _type union field (if there is one) to
// get the most specific class_id possible.
const Resolved = struct {
weak: bool,
ptr: *anyopaque,
class_id: u16,
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry,
finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null,
finalizer_from_zig: ?*const fn (ptr: *anyopaque, session: *Session) void = null,
finalizer: ?Finalizer,
const Finalizer = struct {
// Resolved.ptr is the most specific value in a chain (e.g. IFrame, not EventTarget, Node, ...)
// Finalizer.ptr_id is the most specific value in a chain that defines an acquireRef
ptr_id: usize,
deinit: *const fn (ptr_id: usize, session: *Session) void,
acquireRef: *const fn (ptr_id: usize) void,
release: *const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void,
};
};
pub fn resolveValue(value: anytype) Resolved {
const T = bridge.Struct(@TypeOf(value));
@@ -1153,27 +1158,85 @@ pub fn resolveValue(value: anytype) Resolved {
unreachable;
}
fn resolveT(comptime T: type, value: *anyopaque) Resolved {
fn resolveT(comptime T: type, value: *T) Resolved {
const Meta = T.JsApi.Meta;
return .{
.ptr = value,
.class_id = Meta.class_id,
.prototype_chain = &Meta.prototype_chain,
.weak = if (@hasDecl(Meta, "weak")) Meta.weak else false,
.finalizer_from_v8 = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_v8 else null,
.finalizer_from_zig = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_zig else null,
.finalizer = blk: {
const FT = (comptime findFinalizerType(T)) orelse break :blk null;
const getFinalizerPtr = comptime finalizerPtrGetter(T, FT);
const finalizer_ptr = getFinalizerPtr(value);
const Wrap = struct {
fn deinit(ptr_id: usize, session: *Session) void {
FT.deinit(@ptrFromInt(ptr_id), session);
}
fn acquireRef(ptr_id: usize) void {
FT.acquireRef(@ptrFromInt(ptr_id));
}
fn release(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const identity_finalizer: *Session.FinalizerCallback.Identity = @ptrCast(@alignCast(ptr));
const fc = identity_finalizer.fc;
if (identity_finalizer.identity.identity_map.fetchRemove(fc.resolved_ptr_id)) |kv| {
var global = kv.value;
v8.v8__Global__Reset(&global);
}
FT.releaseRef(@ptrFromInt(fc.finalizer_ptr_id), fc.session);
}
};
break :blk .{
.ptr_id = @intFromPtr(finalizer_ptr),
.deinit = Wrap.deinit,
.acquireRef = Wrap.acquireRef,
.release = Wrap.release,
};
},
};
}
fn conditionallyReference(value: anytype) void {
const T = bridge.Struct(@TypeOf(value));
if (@hasDecl(T, "acquireRef")) {
value.acquireRef();
return;
// Start at the "resolved" type (the most specific) and work our way up the
// prototype chain looking for the type that defines acquireRef
fn findFinalizerType(comptime T: type) ?type {
const S = bridge.Struct(T);
if (@hasDecl(S, "acquireRef")) {
return S;
}
if (@hasField(T, "_proto")) {
conditionallyReference(value._proto);
if (@hasField(S, "_proto")) {
const ProtoPtr = std.meta.fieldInfo(S, ._proto).type;
const ProtoChild = @typeInfo(ProtoPtr).pointer.child;
return findFinalizerType(ProtoChild);
}
return null;
}
// Generate a function that follows the _proto pointer chain to get to the finalizer type
fn finalizerPtrGetter(comptime T: type, comptime FT: type) *const fn (*T) *FT {
const S = bridge.Struct(T);
if (S == FT) {
return struct {
fn get(v: *T) *FT {
return v;
}
}.get;
}
if (@hasField(S, "_proto")) {
const ProtoPtr = std.meta.fieldInfo(S, ._proto).type;
const ProtoChild = @typeInfo(ProtoPtr).pointer.child;
const childGetter = comptime finalizerPtrGetter(ProtoChild, FT);
return struct {
fn get(v: *T) *FT {
return childGetter(v._proto);
}
}.get;
}
@compileError("Cannot find path from " ++ @typeName(T) ++ " to " ++ @typeName(FT));
}
pub fn stackTrace(self: *const Local) !?[]const u8 {
@@ -1381,6 +1444,34 @@ pub fn debugContextId(self: *const Local) i32 {
return v8.v8__Context__DebugContextId(self.handle);
}
fn createFinalizerCallback(
self: *const Local,
// Key in identity map
// The most specific value (KeyboardEvent, not Event)
resolved_ptr_id: usize,
// The most specific value where finalizers are defined
// What actually gets acquired / released / deinit
finalizer_ptr_id: usize,
deinit: *const fn (ptr_id: usize, session: *Session) void,
) !*Session.FinalizerCallback {
const session = self.ctx.session;
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
errdefer session.releaseArena(arena);
const fc = try arena.create(Session.FinalizerCallback);
fc.* = .{
.arena = arena,
.session = session,
._deinit = deinit,
.resolved_ptr_id = resolved_ptr_id,
.finalizer_ptr_id = finalizer_ptr_id,
};
return fc;
}
// Encapsulates a Local and a HandleScope. When we're going from V8->Zig
// we easily get both a Local and a HandleScope via Caller.init.
// But when we're going from Zig -> V8, things are more complicated.

View File

@@ -67,7 +67,7 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo
return .{ .handle = global, .temps = {} };
}
try ctx.trackTemp(global);
return .{ .handle = global, .temps = &ctx.identity.temps };
return .{ .handle = global, .temps = &ctx.session.temps };
}
pub const Temp = G(.temp);

View File

@@ -245,15 +245,37 @@ pub fn toJson(self: Value, allocator: Allocator) ![]u8 {
return js.String.toSliceWithAlloc(.{ .local = local, .handle = str_handle }, allocator);
}
// Currently does not support host objects (Blob, File, etc.) or transferables
// which require delegate callbacks to be implemented.
// Throws a DataCloneError for host objects (Blob, File, etc.) that cannot be serialized.
// Does not support transferables which require additional delegate callbacks.
pub fn structuredClone(self: Value) !Value {
const local = self.local;
const v8_context = local.handle;
const v8_isolate = local.isolate.handle;
const SerializerDelegate = struct {
// Called when V8 encounters a host object it doesn't know how to serialize.
// Returns false to indicate the object cannot be cloned, and throws a DataCloneError.
// V8 asserts has_exception() after this returns false, so we must throw here.
fn writeHostObject(_: ?*anyopaque, isolate: ?*v8.Isolate, _: ?*const v8.Object) callconv(.c) v8.MaybeBool {
const iso = isolate orelse return .{ .has_value = true, .value = false };
const message = v8.v8__String__NewFromUtf8(iso, "The object cannot be cloned.", v8.kNormal, -1);
const error_value = v8.v8__Exception__Error(message) orelse return .{ .has_value = true, .value = false };
_ = v8.v8__Isolate__ThrowException(iso, error_value);
return .{ .has_value = true, .value = false };
}
// Called by V8 to report serialization errors. The exception should already be thrown.
fn throwDataCloneError(_: ?*anyopaque, _: ?*const v8.String) callconv(.c) void {}
};
const size, const data = blk: {
const serializer = v8.v8__ValueSerializer__New(v8_isolate, null) orelse return error.JsException;
const serializer = v8.v8__ValueSerializer__New(v8_isolate, &.{
.data = null,
.get_shared_array_buffer_id = null,
.write_host_object = SerializerDelegate.writeHostObject,
.throw_data_clone_error = SerializerDelegate.throwDataCloneError,
}) orelse return error.JsException;
defer v8.v8__ValueSerializer__DELETE(serializer);
var write_result: v8.MaybeBool = undefined;
@@ -303,7 +325,7 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa
return .{ .handle = global, .temps = {} };
}
try ctx.trackTemp(global);
return .{ .handle = global, .temps = &ctx.identity.temps };
return .{ .handle = global, .temps = &ctx.session.temps };
}
pub fn toZig(self: Value, comptime T: type) !T {

View File

@@ -101,33 +101,6 @@ pub fn Builder(comptime T: type) type {
}
return entries;
}
pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool, session: *Session) void) Finalizer {
return .{
.from_zig = struct {
fn wrap(ptr: *anyopaque, session: *Session) void {
func(@ptrCast(@alignCast(ptr)), true, session);
}
}.wrap,
.from_v8 = struct {
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const fc: *Session.FinalizerCallback = @ptrCast(@alignCast(ptr));
const value_ptr = fc.ptr;
if (fc.identity.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
func(@ptrCast(@alignCast(value_ptr)), false, fc.session);
fc.releaseIdentity();
} else {
// A bit weird, but v8 _requires_ that we release it
// If we don't. We'll 100% crash.
v8.v8__Global__Reset(&fc.global);
}
}
}.wrap,
};
}
};
}
@@ -411,17 +384,6 @@ pub const Property = struct {
}
};
const Finalizer = struct {
// The finalizer wrapper when called from Zig. This is only called on
// Origin.deinit
from_zig: *const fn (ctx: *anyopaque, session: *Session) void,
// The finalizer wrapper when called from V8. This may never be called
// (hence why we fallback to calling in Origin.deinit). If it is called,
// it is only ever called after we SetWeak on the Global.
from_v8: *const fn (?*const v8.WeakCallbackInfo) callconv(.c) void,
};
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller: Caller = undefined;

View File

@@ -148,3 +148,13 @@
}
</script>
<script id=identity>
{
const element = document.createElement('canvas');
const ctx = element.getContext('2d');
testing.expectTrue(ctx === element.getContext('2d'));
testing.expectEqual(null, element.getContext('webgl'));
}
</script>

View File

@@ -85,3 +85,13 @@
loseContext.restoreContext();
}
</script>
<script id=identity>
{
const element = document.createElement('canvas');
const ctx = element.getContext('webgl');
testing.expectTrue(ctx === element.getContext('webgl'));
testing.expectEqual(null, element.getContext('2d'));
}
</script>

View File

@@ -0,0 +1,41 @@
<html lang="en" dir="ltr">
<head>
<script src="../testing.js"></script>
</head>
<body>
<script id="test-document-dir-lang">
// HTMLElement.dir and HTMLElement.lang
testing.expectEqual('ltr', document.documentElement.dir);
testing.expectEqual('en', document.documentElement.lang);
// Document.dir and Document.lang delegate to documentElement
testing.expectEqual('ltr', document.dir);
testing.expectEqual('en', document.lang);
// Setting via document updates the documentElement attribute
document.dir = 'rtl';
testing.expectEqual('rtl', document.documentElement.dir);
testing.expectEqual('rtl', document.documentElement.getAttribute('dir'));
document.lang = 'fr';
testing.expectEqual('fr', document.documentElement.lang);
testing.expectEqual('fr', document.documentElement.getAttribute('lang'));
// Setting via element is reflected in document
document.documentElement.dir = 'ltr';
testing.expectEqual('ltr', document.dir);
document.documentElement.lang = 'de';
testing.expectEqual('de', document.lang);
// div elements also have dir and lang
const div = document.createElement('div');
testing.expectEqual('', div.dir);
testing.expectEqual('', div.lang);
div.dir = 'rtl';
div.lang = 'ar';
testing.expectEqual('rtl', div.dir);
testing.expectEqual('ar', div.lang);
</script>
</body>
</html>

View File

@@ -15,8 +15,9 @@
testing.expectEqual(true, validPlatforms.includes(navigator.platform));
testing.expectEqual('en-US', navigator.language);
testing.expectEqual(true, Array.isArray(navigator.languages));
testing.expectEqual(1, navigator.languages.length);
testing.expectEqual(2, navigator.languages.length);
testing.expectEqual('en-US', navigator.languages[0]);
testing.expectEqual('en', navigator.languages[1]);
testing.expectEqual(true, navigator.onLine);
testing.expectEqual(true, navigator.cookieEnabled);
testing.expectEqual(true, navigator.hardwareConcurrency > 0);

View File

@@ -20,8 +20,8 @@
<script id=urlSearchParams>
const inputs = [
// @ZIGDOM [["over", "9000!!"], ["abc", 123], ["key1", ""], ["key2", ""]],
{over: "9000!!", abc: 123, key1: "", key2: ""},
[["over", "9000!!"], ["abc", "123"], ["key1", ""], ["key2", ""]],
{over: "9000!!", abc: 123, key1: "", key2: ""},
"over=9000!!&abc=123&key1&key2=",
"?over=9000!!&abc=123&key1&key2=",
]
@@ -367,3 +367,49 @@
testing.expectEqual(['3'], ups.getAll('b'));
}
</script>
<script id=arrayOfArrays>
{
const usp = new URLSearchParams([["a", "1"], ["b", "2"], ["a", "3"]]);
testing.expectEqual(3, usp.size);
testing.expectEqual('1', usp.get('a'));
testing.expectEqual(['1', '3'], usp.getAll('a'));
testing.expectEqual('2', usp.get('b'));
testing.expectEqual('a=1&b=2&a=3', usp.toString());
}
</script>
<script id=arrayOfArraysEmpty>
{
const usp = new URLSearchParams([]);
testing.expectEqual(0, usp.size);
testing.expectEqual('', usp.toString());
}
</script>
<script id=arrayOfArraysDuplicateKeys>
{
const usp = new URLSearchParams([["key", "first"], ["key", "second"], ["key", "third"]]);
testing.expectEqual(3, usp.size);
testing.expectEqual('first', usp.get('key'));
testing.expectEqual(['first', 'second', 'third'], usp.getAll('key'));
}
</script>
<script id=arrayOfArraysSpecialChars>
{
const usp = new URLSearchParams([["q", "hello world"], ["url", "https://example.com/?a=1&b=2"]]);
testing.expectEqual(2, usp.size);
testing.expectEqual('hello world', usp.get('q'));
testing.expectEqual('https://example.com/?a=1&b=2', usp.get('url'));
}
</script>
<script id=arrayOfArraysNumericValues>
{
const usp = new URLSearchParams([["count", 42], ["pi", 3.14]]);
testing.expectEqual(2, usp.size);
testing.expectEqual('42', usp.get('count'));
testing.expectEqual('3.14', usp.get('pi'));
}
</script>

View File

@@ -0,0 +1,4 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<div id=sel0>selector-0-content</div>
<div id=sel1>selector-1-content</div>

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<body></body>
<script id=window>
testing.expectEqual(window, globalThis);
@@ -260,6 +261,28 @@
}
testing.expectEqual(true, threw);
}
// Host objects (DOM elements) cannot be cloned - should throw, not crash
{
let threw = false;
try {
structuredClone(document.body);
} catch (err) {
threw = true;
}
testing.expectEqual(true, threw);
}
// Objects containing host objects cannot be cloned - should throw, not crash
{
let threw = false;
try {
structuredClone({ element: document.body });
} catch (err) {
threw = true;
}
testing.expectEqual(true, threw);
}
</script>
<script id=cached_getter_wrong_this>

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Session = @import("../Session.zig");
@@ -31,7 +33,7 @@ const AbstractRange = @This();
pub const _prototype_root = true;
_rc: u8,
_rc: lp.RC(u8) = .{},
_type: Type,
_page_id: u32,
_arena: Allocator,
@@ -44,24 +46,18 @@ _start_container: *Node,
_range_link: std.DoublyLinkedList.Node = .{},
pub fn acquireRef(self: *AbstractRange) void {
self._rc += 1;
self._rc.acquire();
}
pub fn deinit(self: *AbstractRange, shutdown: bool, session: *Session) void {
_ = shutdown;
const rc = self._rc;
if (comptime IS_DEBUG) {
std.debug.assert(rc != 0);
pub fn deinit(self: *AbstractRange, session: *Session) void {
if (session.findPageById(self._page_id)) |page| {
page._live_ranges.remove(&self._range_link);
}
session.releaseArena(self._arena);
}
if (rc == 1) {
if (session.findPageById(self._page_id)) |page| {
page._live_ranges.remove(&self._range_link);
}
session.releaseArena(self._arena);
return;
}
self._rc = rc - 1;
pub fn releaseRef(self: *AbstractRange, session: *Session) void {
self._rc.release(self, session);
}
pub const Type = union(enum) {
@@ -338,8 +334,6 @@ pub const JsApi = struct {
pub const name = "AbstractRange";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(AbstractRange.deinit);
};
pub const startContainer = bridge.accessor(AbstractRange.getStartContainer, null, .{});

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Writer = std.Io.Writer;
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
@@ -25,6 +25,7 @@ const Session = @import("../Session.zig");
const Mime = @import("../Mime.zig");
const Writer = std.Io.Writer;
const Allocator = std.mem.Allocator;
/// https://w3c.github.io/FileAPI/#blob-section
@@ -34,6 +35,7 @@ const Blob = @This();
pub const _prototype_root = true;
_type: Type,
_rc: lp.RC(u32),
_arena: Allocator,
@@ -120,6 +122,7 @@ pub fn initWithMimeValidation(
const self = try arena.create(Blob);
self.* = .{
._rc = .{},
._arena = arena,
._type = .generic,
._slice = data,
@@ -128,11 +131,18 @@ pub fn initWithMimeValidation(
return self;
}
pub fn deinit(self: *Blob, shutdown: bool, session: *Session) void {
_ = shutdown;
pub fn deinit(self: *Blob, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn releaseRef(self: *Blob, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *Blob) void {
self._rc.acquire();
}
const largest_vector = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
/// Array of possible vector sizes for the current arch in decrementing order.
/// We may move this to some file for SIMD helpers in the future.
@@ -325,8 +335,6 @@ pub const JsApi = struct {
pub const name = "Blob";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Blob.deinit);
};
pub const constructor = bridge.constructor(Blob.init, .{});

View File

@@ -59,6 +59,7 @@ pub fn fromError(err: anyerror) ?DOMException {
error.TimeoutError => .{ ._code = .timeout_error },
error.InvalidNodeType => .{ ._code = .invalid_node_type_error },
error.DataClone => .{ ._code = .data_clone_error },
error.InvalidAccessError => .{ ._code = .invalid_access_error },
else => null,
};
}

View File

@@ -61,7 +61,7 @@ _fonts: ?*FontFaceSet = null,
_write_insertion_point: ?*Node = null,
_script_created_parser: ?Parser.Streaming = null,
_adopted_style_sheets: ?js.Object.Global = null,
_selection: Selection = .init,
_selection: Selection = .{ ._rc = .init(1) },
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter
// Incremented during custom element reactions when parsing. When > 0,

View File

@@ -523,6 +523,31 @@ pub fn setDir(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe(comptime .wrap("dir"), .wrap(value), page);
}
// ARIAMixin - ARIA attribute reflection
pub fn getAriaAtomic(self: *const Element) ?[]const u8 {
return self.getAttributeSafe(comptime .wrap("aria-atomic"));
}
pub fn setAriaAtomic(self: *Element, value: ?[]const u8, page: *Page) !void {
if (value) |v| {
try self.setAttributeSafe(comptime .wrap("aria-atomic"), .wrap(v), page);
} else {
try self.removeAttribute(comptime .wrap("aria-atomic"), page);
}
}
pub fn getAriaLive(self: *const Element) ?[]const u8 {
return self.getAttributeSafe(comptime .wrap("aria-live"));
}
pub fn setAriaLive(self: *Element, value: ?[]const u8, page: *Page) !void {
if (value) |v| {
try self.setAttributeSafe(comptime .wrap("aria-live"), .wrap(v), page);
} else {
try self.removeAttribute(comptime .wrap("aria-live"), page);
}
}
pub fn getClassName(self: *const Element) []const u8 {
return self.getAttributeSafe(comptime .wrap("class")) orelse "";
}
@@ -1686,6 +1711,8 @@ pub const JsApi = struct {
pub const localName = bridge.accessor(Element.getLocalName, null, .{});
pub const id = bridge.accessor(Element.getId, Element.setId, .{});
pub const slot = bridge.accessor(Element.getSlot, Element.setSlot, .{});
pub const ariaAtomic = bridge.accessor(Element.getAriaAtomic, Element.setAriaAtomic, .{});
pub const ariaLive = bridge.accessor(Element.getAriaLive, Element.setAriaLive, .{});
pub const dir = bridge.accessor(Element.getDir, Element.setDir, .{});
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});
pub const classList = bridge.accessor(Element.getClassList, Element.setClassList, .{});

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
@@ -55,7 +57,7 @@ _is_trusted: bool = false,
// - 0: no reference, always a transient state going to either 1 or about to be deinit'd
// - 1: either zig or v8 have a reference
// - 2: both zig and v8 have a reference
_rc: u8 = 0,
_rc: lp.RC(u8) = .{},
pub const EventPhase = enum(u8) {
none = 0,
@@ -139,25 +141,16 @@ pub fn initEvent(
}
pub fn acquireRef(self: *Event) void {
self._rc += 1;
self._rc.acquire();
}
pub fn deinit(self: *Event, shutdown: bool, session: *Session) void {
if (shutdown) {
session.releaseArena(self._arena);
return;
}
/// Force cleanup on Session shutdown.
pub fn deinit(self: *Event, session: *Session) void {
session.releaseArena(self._arena);
}
const rc = self._rc;
if (comptime IS_DEBUG) {
std.debug.assert(rc != 0);
}
if (rc == 1) {
session.releaseArena(self._arena);
} else {
self._rc = rc - 1;
}
pub fn releaseRef(self: *Event, session: *Session) void {
self._rc.release(self, session);
}
pub fn as(self: *Event, comptime T: type) *T {
@@ -440,8 +433,6 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Event.deinit);
pub const enumerable = false;
};

View File

@@ -60,7 +60,7 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool {
event._is_trusted = false;
event.acquireRef();
defer event.deinit(false, page._session);
defer _ = event.releaseRef(page._session);
try page._event_manager.dispatch(self, event);
return !event._cancelable or !event._prevent_default;
}

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
@@ -26,7 +27,6 @@ const Blob = @import("Blob.zig");
const File = @This();
/// `File` inherits `Blob`.
_proto: *Blob,
// TODO: Implement File API.
@@ -36,10 +36,6 @@ pub fn init(page: *Page) !*File {
return page._factory.blob(arena, File{ ._proto = undefined });
}
pub fn deinit(self: *File, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(File);
@@ -47,8 +43,6 @@ pub const JsApi = struct {
pub const name = "File";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(File.deinit);
};
pub const constructor = bridge.constructor(File.init, .{});

View File

@@ -17,6 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
@@ -31,6 +33,7 @@ const Allocator = std.mem.Allocator;
/// https://developer.mozilla.org/en-US/docs/Web/API/FileReader
const FileReader = @This();
_rc: lp.RC(u8) = .{},
_page: *Page,
_proto: *EventTarget,
_arena: Allocator,
@@ -70,7 +73,7 @@ pub fn init(page: *Page) !*FileReader {
});
}
pub fn deinit(self: *FileReader, _: bool, session: *Session) void {
pub fn deinit(self: *FileReader, session: *Session) void {
if (self._on_abort) |func| func.release();
if (self._on_error) |func| func.release();
if (self._on_load) |func| func.release();
@@ -81,6 +84,14 @@ pub fn deinit(self: *FileReader, _: bool, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn releaseRef(self: *FileReader, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *FileReader) void {
self._rc.acquire();
}
fn asEventTarget(self: *FileReader) *EventTarget {
return self._proto;
}
@@ -309,8 +320,6 @@ pub const JsApi = struct {
pub const name = "FileReader";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FileReader.deinit);
};
pub const constructor = bridge.constructor(FileReader.init, .{});

View File

@@ -182,6 +182,30 @@ pub fn setLocation(self: *HTMLDocument, url: [:0]const u8, page: *Page) !void {
return page.scheduleNavigation(url, .{ .reason = .script, .kind = .{ .push = null } }, .{ .script = self._proto._page });
}
pub fn getDir(self: *HTMLDocument) []const u8 {
const el = self._proto.getDocumentElement() orelse return "";
const html = el.is(Element.Html) orelse return "";
return html.getDir();
}
pub fn setDir(self: *HTMLDocument, value: []const u8, page: *Page) !void {
const el = self._proto.getDocumentElement() orelse return;
const html = el.is(Element.Html) orelse return;
try html.setDir(value, page);
}
pub fn getLang(self: *HTMLDocument) []const u8 {
const el = self._proto.getDocumentElement() orelse return "";
const html = el.is(Element.Html) orelse return "";
return html.getLang();
}
pub fn setLang(self: *HTMLDocument, value: []const u8, page: *Page) !void {
const el = self._proto.getDocumentElement() orelse return;
const html = el.is(Element.Html) orelse return;
try html.setLang(value, page);
}
pub fn getAll(self: *HTMLDocument, page: *Page) !*collections.HTMLAllCollection {
return page._factory.create(collections.HTMLAllCollection.init(self.asNode(), page));
}
@@ -250,8 +274,10 @@ pub const JsApi = struct {
});
}
pub const dir = bridge.accessor(HTMLDocument.getDir, HTMLDocument.setDir, .{});
pub const head = bridge.accessor(HTMLDocument.getHead, null, .{});
pub const body = bridge.accessor(HTMLDocument.getBody, null, .{});
pub const lang = bridge.accessor(HTMLDocument.getLang, HTMLDocument.setLang, .{});
pub const title = bridge.accessor(HTMLDocument.getTitle, HTMLDocument.setTitle, .{});
pub const images = bridge.accessor(HTMLDocument.getImages, null, .{});
pub const scripts = bridge.accessor(HTMLDocument.getScripts, null, .{});

View File

@@ -16,6 +16,8 @@
// 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 lp = @import("lightpanda");
const js = @import("../js/js.zig");
const log = @import("../../log.zig");
@@ -39,6 +41,7 @@ pub fn registerTypes() []const type {
const IntersectionObserver = @This();
_rc: lp.RC(u8) = .{},
_arena: Allocator,
_callback: js.Function.Temp,
_observing: std.ArrayList(*Element) = .{},
@@ -108,15 +111,22 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I
return self;
}
pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void {
pub fn deinit(self: *IntersectionObserver, session: *Session) void {
self._callback.release();
if ((comptime IS_DEBUG) and !shutdown) {
std.debug.assert(self._observing.items.len == 0);
for (self._pending_entries.items) |entry| {
entry.deinitIfUnused(session);
}
session.releaseArena(self._arena);
}
pub fn releaseRef(self: *IntersectionObserver, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *IntersectionObserver) void {
self._rc.acquire();
}
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
// Check if already observing this target
for (self._observing.items) |elem| {
@@ -127,7 +137,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
// Register with page if this is our first observation
if (self._observing.items.len == 0) {
page.js.strongRef(self);
self._rc._refs += 1;
try page.registerIntersectionObserver(self);
}
@@ -144,17 +154,19 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
}
pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) void {
const original_length = self._observing.items.len;
for (self._observing.items, 0..) |elem, i| {
if (elem == target) {
_ = self._observing.swapRemove(i);
_ = self._previous_states.remove(target);
// Remove any pending entries for this target
// Remove any pending entries for this target.
// Entries will be cleaned up by V8 GC via the finalizer.
var j: usize = 0;
while (j < self._pending_entries.items.len) {
if (self._pending_entries.items[j]._target == target) {
const entry = self._pending_entries.swapRemove(j);
entry.deinit(false, page._session);
entry.deinitIfUnused(page._session);
} else {
j += 1;
}
@@ -163,21 +175,26 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi
}
}
if (self._observing.items.len == 0) {
page.js.safeWeakRef(self);
if (original_length > 0 and self._observing.items.len == 0) {
self._rc._refs -= 1;
}
}
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
page.unregisterIntersectionObserver(self);
self._observing.clearRetainingCapacity();
self._previous_states.clearRetainingCapacity();
for (self._pending_entries.items) |entry| {
entry.deinit(false, page._session);
entry.deinitIfUnused(page._session);
}
self._pending_entries.clearRetainingCapacity();
page.js.safeWeakRef(self);
self._previous_states.clearRetainingCapacity();
const observing_count = self._observing.items.len;
self._observing.clearRetainingCapacity();
page.unregisterIntersectionObserver(self);
if (observing_count > 0) {
_ = self.releaseRef(page._session);
}
}
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
@@ -268,7 +285,6 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
._bounding_client_rect = try page._factory.create(data.bounding_client_rect),
._intersection_ratio = data.intersection_ratio,
};
try self._pending_entries.append(self._arena, entry);
}
@@ -310,6 +326,7 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
}
pub const IntersectionObserverEntry = struct {
_rc: lp.RC(u8) = .{},
_arena: Allocator,
_time: f64,
_target: *Element,
@@ -319,10 +336,25 @@ pub const IntersectionObserverEntry = struct {
_intersection_ratio: f64,
_is_intersecting: bool,
pub fn deinit(self: *IntersectionObserverEntry, _: bool, session: *Session) void {
pub fn deinit(self: *IntersectionObserverEntry, session: *Session) void {
session.releaseArena(self._arena);
}
fn deinitIfUnused(self: *IntersectionObserverEntry, session: *Session) void {
if (self._rc._refs == 0) {
// hasn't been handed to JS yet.
self.deinit(session);
}
}
pub fn releaseRef(self: *IntersectionObserverEntry, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *IntersectionObserverEntry) void {
self._rc.acquire();
}
pub fn getTarget(self: *const IntersectionObserverEntry) *Element {
return self._target;
}
@@ -358,8 +390,6 @@ pub const IntersectionObserverEntry = struct {
pub const name = "IntersectionObserverEntry";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(IntersectionObserverEntry.deinit);
};
pub const target = bridge.accessor(IntersectionObserverEntry.getTarget, null, .{});
@@ -379,8 +409,6 @@ pub const JsApi = struct {
pub const name = "IntersectionObserver";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
};
pub const constructor = bridge.constructor(init, .{});

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const String = @import("../../string.zig").String;
const js = @import("../js/js.zig");
@@ -39,6 +40,7 @@ pub fn registerTypes() []const type {
const MutationObserver = @This();
_rc: lp.RC(u8) = .{},
_arena: Allocator,
_callback: js.Function.Temp,
_observing: std.ArrayList(Observing) = .{},
@@ -85,15 +87,20 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
return self;
}
pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void {
/// Force cleanup on Session shutdown.
pub fn deinit(self: *MutationObserver, session: *Session) void {
self._callback.release();
if ((comptime IS_DEBUG) and !shutdown) {
std.debug.assert(self._observing.items.len == 0);
}
session.releaseArena(self._arena);
}
pub fn releaseRef(self: *MutationObserver, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *MutationObserver) void {
self._rc.acquire();
}
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
const arena = self._arena;
@@ -158,7 +165,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
// Register with page if this is our first observation
if (self._observing.items.len == 0) {
page.js.strongRef(self);
self._rc._refs += 1;
try page.registerMutationObserver(self);
}
@@ -169,13 +176,17 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
}
pub fn disconnect(self: *MutationObserver, page: *Page) void {
page.unregisterMutationObserver(self);
self._observing.clearRetainingCapacity();
for (self._pending_records.items) |record| {
record.deinit(false, page._session);
_ = record.releaseRef(page._session);
}
self._pending_records.clearRetainingCapacity();
page.js.safeWeakRef(self);
const observing_count = self._observing.items.len;
self._observing.clearRetainingCapacity();
if (observing_count > 0) {
_ = self.releaseRef(page._session);
}
page.unregisterMutationObserver(self);
}
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
@@ -348,6 +359,7 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
}
pub const MutationRecord = struct {
_rc: lp.RC(u8) = .{},
_type: Type,
_target: *Node,
_arena: Allocator,
@@ -364,10 +376,18 @@ pub const MutationRecord = struct {
characterData,
};
pub fn deinit(self: *MutationRecord, _: bool, session: *Session) void {
pub fn deinit(self: *MutationRecord, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn releaseRef(self: *MutationRecord, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *MutationRecord) void {
self._rc.acquire();
}
pub fn getType(self: *const MutationRecord) []const u8 {
return switch (self._type) {
.attributes => "attributes",
@@ -418,8 +438,6 @@ pub const MutationRecord = struct {
pub const name = "MutationRecord";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MutationRecord.deinit);
};
pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{});
@@ -441,8 +459,6 @@ pub const JsApi = struct {
pub const name = "MutationObserver";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MutationObserver.deinit);
};
pub const constructor = bridge.constructor(MutationObserver.init, .{});

View File

@@ -40,8 +40,8 @@ pub fn getUserAgent(_: *const Navigator, page: *Page) []const u8 {
return page._session.browser.app.config.http_headers.user_agent;
}
pub fn getLanguages(_: *const Navigator) [1][]const u8 {
return .{"en-US"};
pub fn getLanguages(_: *const Navigator) [2][]const u8 {
return .{ "en-US", "en" };
}
pub fn getPlatform(_: *const Navigator) []const u8 {

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
@@ -50,14 +51,23 @@ pub fn query(_: *const Permissions, qd: QueryDescriptor, page: *Page) !js.Promis
}
const PermissionStatus = struct {
_rc: lp.RC(u8) = .{},
_arena: Allocator,
_name: []const u8,
_state: []const u8,
pub fn deinit(self: *PermissionStatus, _: bool, session: *Session) void {
pub fn deinit(self: *PermissionStatus, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn releaseRef(self: *PermissionStatus, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *PermissionStatus) void {
self._rc.acquire();
}
fn getName(self: *const PermissionStatus) []const u8 {
return self._name;
}
@@ -72,8 +82,6 @@ const PermissionStatus = struct {
pub const name = "PermissionStatus";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PermissionStatus.deinit);
};
pub const name = bridge.accessor(getName, null, .{});
pub const state = bridge.accessor(getState, null, .{});

View File

@@ -38,10 +38,6 @@ pub fn init(page: *Page) !*Range {
return page._factory.abstractRange(arena, Range{ ._proto = undefined }, page);
}
pub fn deinit(self: *Range, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asAbstractRange(self: *Range) *AbstractRange {
return self._proto;
}
@@ -697,8 +693,6 @@ pub const JsApi = struct {
pub const name = "Range";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Range.deinit);
};
// Constants for compareBoundaryPoints

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
@@ -32,18 +33,27 @@ const Selection = @This();
pub const SelectionDirection = enum { backward, forward, none };
_rc: lp.RC(u8) = .{},
_range: ?*Range = null,
_direction: SelectionDirection = .none,
pub const init: Selection = .{};
pub fn deinit(self: *Selection, shutdown: bool, session: *Session) void {
pub fn deinit(self: *Selection, session: *Session) void {
if (self._range) |r| {
r.deinit(shutdown, session);
r.asAbstractRange().releaseRef(session);
self._range = null;
}
}
pub fn releaseRef(self: *Selection, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *Selection) void {
self._rc.acquire();
}
fn dispatchSelectionChangeEvent(page: *Page) !void {
const event = try Event.init("selectionchange", .{}, page);
try page._event_manager.dispatch(page.document.asEventTarget(), event);
@@ -693,7 +703,7 @@ pub fn toString(self: *const Selection, page: *Page) ![]const u8 {
fn setRange(self: *Selection, new_range: ?*Range, page: *Page) void {
if (self._range) |existing| {
existing.deinit(false, page._session);
_ = existing.asAbstractRange().releaseRef(page._session);
}
if (new_range) |nr| {
nr.asAbstractRange().acquireRef();
@@ -708,7 +718,6 @@ pub const JsApi = struct {
pub const name = "Selection";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const finalizer = bridge.finalizer(Selection.deinit);
};
pub const anchorNode = bridge.accessor(Selection.getAnchorNode, null, .{});

View File

@@ -256,8 +256,7 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 {
.{ page.origin orelse "null", uuid_buf },
);
try page._blob_urls.put(page.arena, blob_url, blob);
// prevent GC from cleaning up the blob while it's in the registry
page.js.strongRef(blob);
blob.acquireRef();
return blob_url;
}
@@ -267,9 +266,8 @@ pub fn revokeObjectURL(url: []const u8, page: *Page) void {
return;
}
// Remove from registry and release strong ref (no-op if not found)
if (page._blob_urls.fetchRemove(url)) |entry| {
page.js.weakRef(entry.value);
entry.value.releaseRef(page._session);
}
}

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const log = @import("../../../log.zig");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
@@ -33,6 +34,7 @@ const PlayState = enum {
finished,
};
_rc: lp.RC(u32) = .{},
_page: *Page,
_arena: Allocator,
@@ -62,10 +64,18 @@ pub fn init(page: *Page) !*Animation {
return self;
}
pub fn deinit(self: *Animation, _: bool, session: *Session) void {
pub fn deinit(self: *Animation, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn releaseRef(self: *Animation, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *Animation) void {
self._rc.acquire();
}
pub fn play(self: *Animation, page: *Page) !void {
if (self._playState == .running) {
return;
@@ -75,7 +85,7 @@ pub fn play(self: *Animation, page: *Page) !void {
self._playState = .running;
// Schedule the transition from .running => .finished in 10ms.
page.js.strongRef(self);
self.acquireRef();
try page.js.scheduler.add(
self,
Animation.update,
@@ -201,7 +211,7 @@ fn update(ctx: *anyopaque) !?u32 {
}
// No future change scheduled, set the object weak for garbage collection.
self._page.js.weakRef(self);
self.releaseRef(self._page._session);
return null;
}
@@ -220,8 +230,6 @@ pub const JsApi = struct {
pub const name = "Animation";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Animation.deinit);
};
pub const play = bridge.function(Animation.play, .{});

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const log = @import("../../../log.zig");
const js = @import("../../js/js.zig");
@@ -37,15 +38,9 @@ _data: union(enum) {
radio_node_list: *RadioNodeList,
name: NodeLive(.name),
},
_rc: usize = 0,
pub fn deinit(self: *NodeList, _: bool, session: *Session) void {
const rc = self._rc;
if (rc > 1) {
self._rc = rc - 1;
return;
}
_rc: lp.RC(u32) = .{},
pub fn deinit(self: *NodeList, session: *Session) void {
switch (self._data) {
.selector_list => |list| list.deinit(session),
.child_nodes => |cn| cn.deinit(session),
@@ -53,8 +48,12 @@ pub fn deinit(self: *NodeList, _: bool, session: *Session) void {
}
}
pub fn releaseRef(self: *NodeList, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *NodeList) void {
self._rc += 1;
self._rc.acquire();
}
pub fn length(self: *NodeList, page: *Page) !u32 {
@@ -119,8 +118,12 @@ const Iterator = struct {
const Entry = struct { u32, *Node };
pub fn deinit(self: *Iterator, shutdown: bool, session: *Session) void {
self.list.deinit(shutdown, session);
pub fn deinit(self: *Iterator, session: *Session) void {
self.list.deinit(session);
}
pub fn releaseRef(self: *Iterator, session: *Session) void {
self.list.releaseRef(session);
}
pub fn acquireRef(self: *Iterator) void {
@@ -143,8 +146,6 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false;
pub const weak = true;
pub const finalizer = bridge.finalizer(NodeList.deinit);
};
pub const length = bridge.accessor(NodeList.length, null, .{});

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
@@ -40,9 +41,15 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
return page._factory.create(Self{ .inner = inner });
}
pub fn deinit(self: *Self, shutdown: bool, session: *Session) void {
if (@hasDecl(Inner, "deinit")) {
self.inner.deinit(shutdown, session);
pub fn deinit(self: *Self, session: *Session) void {
_ = self;
_ = session;
}
pub fn releaseRef(self: *Self, session: *Session) void {
// Release the reference to the inner type that we acquired
if (@hasDecl(Inner, "releaseRef")) {
self.inner.releaseRef(session);
}
}
@@ -73,8 +80,6 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
pub const Meta = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Self.deinit);
};
pub const next = bridge.function(Self.next, .{ .null_as_undefined = true });

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
@@ -25,6 +26,7 @@ const Allocator = std.mem.Allocator;
const FontFace = @This();
_rc: lp.RC(u8) = .{},
_arena: Allocator,
_family: []const u8,
@@ -42,10 +44,18 @@ pub fn init(family: []const u8, source: []const u8, page: *Page) !*FontFace {
return self;
}
pub fn deinit(self: *FontFace, _: bool, session: *Session) void {
pub fn deinit(self: *FontFace, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn releaseRef(self: *FontFace, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *FontFace) void {
self._rc.acquire();
}
pub fn getFamily(self: *const FontFace) []const u8 {
return self._family;
}
@@ -67,8 +77,6 @@ pub const JsApi = struct {
pub const name = "FontFace";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FontFace.deinit);
};
pub const constructor = bridge.constructor(FontFace.init, .{});

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
@@ -28,6 +29,7 @@ const Allocator = std.mem.Allocator;
const FontFaceSet = @This();
_rc: lp.RC(u8) = .{},
_proto: *EventTarget,
_arena: Allocator,
@@ -41,10 +43,18 @@ pub fn init(page: *Page) !*FontFaceSet {
});
}
pub fn deinit(self: *FontFaceSet, _: bool, session: *Session) void {
pub fn deinit(self: *FontFaceSet, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn releaseRef(self: *FontFaceSet, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *FontFaceSet) void {
self._rc.acquire();
}
pub fn asEventTarget(self: *FontFaceSet) *EventTarget {
return self._proto;
}
@@ -95,8 +105,6 @@ pub const JsApi = struct {
pub const name = "FontFaceSet";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FontFaceSet.deinit);
};
pub const size = bridge.property(0, .{ .template = false, .readonly = true });

View File

@@ -26,6 +26,8 @@ const GenericIterator = @import("../collections/iterator.zig").Entry;
const Page = @import("../../Page.zig");
const String = @import("../../../string.zig").String;
const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
pub fn registerTypes() []const type {
@@ -419,7 +421,7 @@ pub fn validateAttributeName(name: String) !void {
}
}
pub fn normalizeNameForLookup(name: String, page: *Page) !String {
fn normalizeNameForLookup(name: String, page: *Page) !String {
if (!needsLowerCasing(name.str())) {
return name;
}
@@ -431,6 +433,14 @@ pub fn normalizeNameForLookup(name: String, page: *Page) !String {
return .wrap(normalized);
}
pub fn normalizeNameForLookupAlloc(allocator: Allocator, name: String) !String {
if (!needsLowerCasing(name.str())) {
return name.dupe(allocator);
}
const normalized = try std.ascii.allocLowerString(allocator, name.str());
return .wrap(normalized);
}
fn needsLowerCasing(name: []const u8) bool {
var remaining = name;
if (comptime std.simd.suggestVectorLength(u8)) |vector_len| {

View File

@@ -375,6 +375,30 @@ pub fn setTabIndex(self: *HtmlElement, value: i32, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("tabindex"), .wrap(str), page);
}
pub fn getDir(self: *HtmlElement) []const u8 {
return self.asElement().getAttributeSafe(comptime .wrap("dir")) orelse "";
}
pub fn setDir(self: *HtmlElement, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("dir"), .wrap(value), page);
}
pub fn getLang(self: *HtmlElement) []const u8 {
return self.asElement().getAttributeSafe(comptime .wrap("lang")) orelse "";
}
pub fn setLang(self: *HtmlElement, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("lang"), .wrap(value), page);
}
pub fn getTitle(self: *HtmlElement) []const u8 {
return self.asElement().getAttributeSafe(comptime .wrap("title")) orelse "";
}
pub fn setTitle(self: *HtmlElement, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("title"), .wrap(value), page);
}
pub fn getAttributeFunction(
self: *HtmlElement,
listener_type: GlobalEventHandler,
@@ -1211,8 +1235,11 @@ pub const JsApi = struct {
pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true });
pub const click = bridge.function(HtmlElement.click, .{});
pub const dir = bridge.accessor(HtmlElement.getDir, HtmlElement.setDir, .{});
pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{});
pub const lang = bridge.accessor(HtmlElement.getLang, HtmlElement.setLang, .{});
pub const tabIndex = bridge.accessor(HtmlElement.getTabIndex, HtmlElement.setTabIndex, .{});
pub const title = bridge.accessor(HtmlElement.getTitle, HtmlElement.setTitle, .{});
pub const onabort = bridge.accessor(HtmlElement.getOnAbort, HtmlElement.setOnAbort, .{});
pub const onanimationcancel = bridge.accessor(HtmlElement.getOnAnimationCancel, HtmlElement.setOnAnimationCancel, .{});

View File

@@ -174,6 +174,14 @@ pub fn setType(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("type"), .wrap(value), page);
}
pub fn getRel(self: *Anchor) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("rel")) orelse "";
}
pub fn setRel(self: *Anchor, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("rel"), .wrap(value), page);
}
pub fn getName(self: *const Anchor) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("name")) orelse "";
}
@@ -218,6 +226,7 @@ pub const JsApi = struct {
pub const pathname = bridge.accessor(Anchor.getPathname, Anchor.setPathname, .{});
pub const search = bridge.accessor(Anchor.getSearch, Anchor.setSearch, .{});
pub const hash = bridge.accessor(Anchor.getHash, Anchor.setHash, .{});
pub const rel = bridge.accessor(Anchor.getRel, Anchor.setRel, .{});
pub const @"type" = bridge.accessor(Anchor.getType, Anchor.setType, .{});
pub const text = bridge.accessor(Anchor.getText, Anchor.setText, .{});
pub const relList = bridge.accessor(_getRelList, null, .{ .null_as_undefined = true });

View File

@@ -29,6 +29,9 @@ const OffscreenCanvas = @import("../../canvas/OffscreenCanvas.zig");
const Canvas = @This();
_proto: *HtmlElement,
_cached: ?DrawingContext = null,
const ContextType = enum { none, @"2d", webgl };
pub fn asElement(self: *Canvas) *Element {
return self._proto._proto;
@@ -68,17 +71,28 @@ const DrawingContext = union(enum) {
};
pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext {
if (std.mem.eql(u8, context_type, "2d")) {
const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self });
return .{ .@"2d" = ctx };
if (self._cached) |cached| {
const matches = switch (cached) {
.@"2d" => std.mem.eql(u8, context_type, "2d"),
.webgl => std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl"),
};
return if (matches) cached else null;
}
if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) {
const ctx = try page._factory.create(WebGLRenderingContext{});
return .{ .webgl = ctx };
}
const drawing_context: DrawingContext = blk: {
if (std.mem.eql(u8, context_type, "2d")) {
const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self });
break :blk .{ .@"2d" = ctx };
}
return null;
if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) {
const ctx = try page._factory.create(WebGLRenderingContext{});
break :blk .{ .webgl = ctx };
}
return null;
};
self._cached = drawing_context;
return drawing_context;
}
/// Transfers control of the canvas to an OffscreenCanvas.

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
@@ -25,6 +26,7 @@ const Allocator = std.mem.Allocator;
const TextDecoder = @This();
_rc: lp.RC(u8) = .{},
_fatal: bool,
_arena: Allocator,
_ignore_bom: bool,
@@ -60,10 +62,18 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder {
return self;
}
pub fn deinit(self: *TextDecoder, _: bool, session: *Session) void {
pub fn deinit(self: *TextDecoder, session: *Session) void {
session.releaseArena(self._arena);
}
pub fn releaseRef(self: *TextDecoder, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *TextDecoder) void {
self._rc.acquire();
}
pub fn getIgnoreBOM(self: *const TextDecoder) bool {
return self._ignore_bom;
}
@@ -109,8 +119,6 @@ pub const JsApi = struct {
pub const name = "TextDecoder";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(TextDecoder.deinit);
};
pub const constructor = bridge.constructor(TextDecoder.init, .{});

View File

@@ -53,10 +53,6 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CompositionEvent {
return event;
}
pub fn deinit(self: *CompositionEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *CompositionEvent) *Event {
return self._proto;
}
@@ -72,8 +68,6 @@ pub const JsApi = struct {
pub const name = "CompositionEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(CompositionEvent.deinit);
};
pub const constructor = bridge.constructor(CompositionEvent.init, .{});

View File

@@ -73,11 +73,19 @@ pub fn initCustomEvent(
self._detail = detail_;
}
pub fn deinit(self: *CustomEvent, shutdown: bool, session: *Session) void {
pub fn deinit(self: *CustomEvent, session: *Session) void {
if (self._detail) |d| {
d.release();
}
self._proto.deinit(shutdown, session);
self._proto.deinit(session);
}
pub fn acquireRef(self: *CustomEvent) void {
self._proto.acquireRef();
}
pub fn releaseRef(self: *CustomEvent, session: *Session) void {
self._proto._rc.release(self, session);
}
pub fn asEvent(self: *CustomEvent) *Event {
@@ -95,8 +103,6 @@ pub const JsApi = struct {
pub const name = "CustomEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(CustomEvent.deinit);
pub const enumerable = false;
};

View File

@@ -80,11 +80,19 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
return event;
}
pub fn deinit(self: *ErrorEvent, shutdown: bool, session: *Session) void {
pub fn deinit(self: *ErrorEvent, session: *Session) void {
if (self._error) |e| {
e.release();
}
self._proto.deinit(shutdown, session);
self._proto.deinit(session);
}
pub fn acquireRef(self: *ErrorEvent) void {
self._proto.acquireRef();
}
pub fn releaseRef(self: *ErrorEvent, session: *Session) void {
self._proto._rc.release(self, session);
}
pub fn asEvent(self: *ErrorEvent) *Event {

View File

@@ -70,10 +70,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event;
}
pub fn deinit(self: *FocusEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *FocusEvent) *Event {
return self._proto.asEvent();
}
@@ -89,8 +85,6 @@ pub const JsApi = struct {
pub const name = "FocusEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FocusEvent.deinit);
};
pub const constructor = bridge.constructor(FocusEvent.init, .{});

View File

@@ -66,10 +66,6 @@ fn initWithTrusted(arena: Allocator, typ: String, maybe_options: ?Options, trust
return event;
}
pub fn deinit(self: *FormDataEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *FormDataEvent) *Event {
return self._proto;
}
@@ -85,8 +81,6 @@ pub const JsApi = struct {
pub const name = "FormDataEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FormDataEvent.deinit);
};
pub const constructor = bridge.constructor(FormDataEvent.init, .{});

View File

@@ -83,10 +83,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event;
}
pub fn deinit(self: *InputEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *InputEvent) *Event {
return self._proto.asEvent();
}
@@ -110,8 +106,6 @@ pub const JsApi = struct {
pub const name = "InputEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(InputEvent.deinit);
};
pub const constructor = bridge.constructor(InputEvent.init, .{});

View File

@@ -229,10 +229,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event;
}
pub fn deinit(self: *KeyboardEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *KeyboardEvent) *Event {
return self._proto.asEvent();
}
@@ -296,8 +292,6 @@ pub const JsApi = struct {
pub const name = "KeyboardEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(KeyboardEvent.deinit);
};
pub const constructor = bridge.constructor(KeyboardEvent.init, .{});

View File

@@ -73,11 +73,19 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
return event;
}
pub fn deinit(self: *MessageEvent, shutdown: bool, session: *Session) void {
pub fn deinit(self: *MessageEvent, session: *Session) void {
if (self._data) |d| {
d.release();
}
self._proto.deinit(shutdown, session);
self._proto.deinit(session);
}
pub fn acquireRef(self: *MessageEvent) void {
self._proto.acquireRef();
}
pub fn releaseRef(self: *MessageEvent, session: *Session) void {
self._proto._rc.release(self, session);
}
pub fn asEvent(self: *MessageEvent) *Event {
@@ -103,8 +111,6 @@ pub const JsApi = struct {
pub const name = "MessageEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MessageEvent.deinit);
};
pub const constructor = bridge.constructor(MessageEvent.init, .{});

View File

@@ -121,10 +121,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event;
}
pub fn deinit(self: *MouseEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *MouseEvent) *Event {
return self._proto.asEvent();
}
@@ -203,8 +199,6 @@ pub const JsApi = struct {
pub const name = "MouseEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MouseEvent.deinit);
};
pub const constructor = bridge.constructor(MouseEvent.init, .{});

View File

@@ -83,10 +83,6 @@ fn initWithTrusted(
return event;
}
pub fn deinit(self: *NavigationCurrentEntryChangeEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event {
return self._proto;
}
@@ -106,8 +102,6 @@ pub const JsApi = struct {
pub const name = "NavigationCurrentEntryChangeEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(NavigationCurrentEntryChangeEvent.deinit);
};
pub const constructor = bridge.constructor(NavigationCurrentEntryChangeEvent.init, .{});

View File

@@ -66,10 +66,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event;
}
pub fn deinit(self: *PageTransitionEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *PageTransitionEvent) *Event {
return self._proto;
}
@@ -85,8 +81,6 @@ pub const JsApi = struct {
pub const name = "PageTransitionEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PageTransitionEvent.deinit);
};
pub const constructor = bridge.constructor(PageTransitionEvent.init, .{});

View File

@@ -128,10 +128,6 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PointerEvent {
return event;
}
pub fn deinit(self: *PointerEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *PointerEvent) *Event {
return self._proto.asEvent();
}
@@ -191,8 +187,6 @@ pub const JsApi = struct {
pub const name = "PointerEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PointerEvent.deinit);
};
pub const constructor = bridge.constructor(PointerEvent.init, .{});

View File

@@ -67,10 +67,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event;
}
pub fn deinit(self: *PopStateEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *PopStateEvent) *Event {
return self._proto;
}
@@ -92,8 +88,6 @@ pub const JsApi = struct {
pub const name = "PopStateEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PopStateEvent.deinit);
};
pub const constructor = bridge.constructor(PopStateEvent.init, .{});

View File

@@ -68,10 +68,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event;
}
pub fn deinit(self: *ProgressEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *ProgressEvent) *Event {
return self._proto;
}
@@ -96,8 +92,6 @@ pub const JsApi = struct {
pub const name = "ProgressEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(ProgressEvent.deinit);
};
pub const constructor = bridge.constructor(ProgressEvent.init, .{});

View File

@@ -56,14 +56,22 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*PromiseRejectionEve
return event;
}
pub fn deinit(self: *PromiseRejectionEvent, shutdown: bool, session: *Session) void {
pub fn deinit(self: *PromiseRejectionEvent, session: *Session) void {
if (self._reason) |r| {
r.release();
}
if (self._promise) |p| {
p.release();
}
self._proto.deinit(shutdown, session);
self._proto.deinit(session);
}
pub fn acquireRef(self: *PromiseRejectionEvent) void {
self._proto.acquireRef();
}
pub fn releaseRef(self: *PromiseRejectionEvent, session: *Session) void {
self._proto._rc.release(self, session);
}
pub fn asEvent(self: *PromiseRejectionEvent) *Event {
@@ -85,8 +93,6 @@ pub const JsApi = struct {
pub const name = "PromiseRejectionEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PromiseRejectionEvent.deinit);
};
pub const constructor = bridge.constructor(PromiseRejectionEvent.init, .{});

View File

@@ -67,10 +67,6 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event;
}
pub fn deinit(self: *SubmitEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *SubmitEvent) *Event {
return self._proto;
}
@@ -86,8 +82,6 @@ pub const JsApi = struct {
pub const name = "SubmitEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(SubmitEvent.deinit);
};
pub const constructor = bridge.constructor(SubmitEvent.init, .{});

View File

@@ -59,10 +59,6 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*TextEvent {
return event;
}
pub fn deinit(self: *TextEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *TextEvent) *Event {
return self._proto.asEvent();
}
@@ -101,8 +97,6 @@ pub const JsApi = struct {
pub const name = "TextEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(TextEvent.deinit);
};
// No constructor - TextEvent is created via document.createEvent('TextEvent')

View File

@@ -71,10 +71,6 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent {
return event;
}
pub fn deinit(self: *UIEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn as(self: *UIEvent, comptime T: type) *T {
return self.is(T).?;
}
@@ -122,8 +118,6 @@ pub const JsApi = struct {
pub const name = "UIEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(UIEvent.deinit);
};
pub const constructor = bridge.constructor(UIEvent.init, .{});

View File

@@ -87,10 +87,6 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*WheelEvent {
return event;
}
pub fn deinit(self: *WheelEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *WheelEvent) *Event {
return self._proto.asEvent();
}
@@ -118,8 +114,6 @@ pub const JsApi = struct {
pub const name = "WheelEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(WheelEvent.deinit);
};
pub const constructor = bridge.constructor(WheelEvent.init, .{});

View File

@@ -62,7 +62,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
}
const response = try Response.init(null, .{ .status = 0 }, page);
errdefer response.deinit(true, page._session);
errdefer response.deinit(page._session);
const fetch = try response._arena.create(Fetch);
fetch.* = .{
@@ -226,7 +226,7 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
return ls.toLocal(self._resolver).resolve("fetch done", js_val);
}
fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
fn httpErrorCallback(ctx: *anyopaque, _: anyerror) void {
const self: *Fetch = @ptrCast(@alignCast(ctx));
var response = self._response;
@@ -235,7 +235,7 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
// clear this. (defer since `self is in the response's arena).
defer if (self._owns_response) {
response.deinit(err == error.Abort, self._page._session);
response.deinit(self._page._session);
self._owns_response = false;
};
@@ -257,7 +257,7 @@ fn httpShutdownCallback(ctx: *anyopaque) void {
if (self._owns_response) {
var response = self._response;
response._transfer = null;
response.deinit(true, self._page._session);
response.deinit(self._page._session);
// Do not access `self` after this point: the Fetch struct was
// allocated from response._arena which has been released.
}

View File

@@ -86,8 +86,8 @@ pub fn forEach(self: *Headers, cb_: js.Function, js_this_: ?js.Object) !void {
}
// TODO: do we really need 2 different header structs??
const net_http = @import("../../../network/http.zig");
pub fn populateHttpHeader(self: *Headers, allocator: Allocator, http_headers: *net_http.Headers) !void {
const http = @import("../../../network/http.zig");
pub fn populateHttpHeader(self: *Headers, allocator: Allocator, http_headers: *http.Headers) !void {
for (self._list._entries.items) |entry| {
const merged = try std.mem.concatWithSentinel(allocator, u8, &.{ entry.name.str(), ": ", entry.value.str() }, 0);
try http_headers.add(merged);

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const net_http = @import("../../../network/http.zig");
const http = @import("../../../network/http.zig");
const URL = @import("../URL.zig");
const Page = @import("../../Page.zig");
@@ -31,7 +31,7 @@ const Allocator = std.mem.Allocator;
const Request = @This();
_url: [:0]const u8,
_method: net_http.Method,
_method: http.Method,
_headers: ?*Headers,
_body: ?[]const u8,
_arena: Allocator,
@@ -119,14 +119,14 @@ pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request {
});
}
fn parseMethod(method: []const u8, page: *Page) !net_http.Method {
fn parseMethod(method: []const u8, page: *Page) !http.Method {
if (method.len > "propfind".len) {
return error.InvalidMethod;
}
const lower = std.ascii.lowerString(&page.buf, method);
const method_lookup = std.StaticStringMap(net_http.Method).initComptime(.{
const method_lookup = std.StaticStringMap(http.Method).initComptime(.{
.{ "get", .GET },
.{ "post", .POST },
.{ "delete", .DELETE },

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const HttpClient = @import("../../HttpClient.zig");
@@ -38,6 +39,7 @@ pub const Type = enum {
opaqueredirect,
};
_rc: lp.RC(u8) = .{},
_status: u16,
_arena: Allocator,
_headers: *Headers,
@@ -78,18 +80,22 @@ pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response {
return self;
}
pub fn deinit(self: *Response, shutdown: bool, session: *Session) void {
pub fn deinit(self: *Response, session: *Session) void {
if (self._transfer) |transfer| {
if (shutdown) {
transfer.terminate();
} else {
transfer.abort(error.Abort);
}
transfer.abort(error.Abort);
self._transfer = null;
}
session.releaseArena(self._arena);
}
pub fn releaseRef(self: *Response, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *Response) void {
self._rc.acquire();
}
pub fn getStatus(self: *const Response) u16 {
return self._status;
}
@@ -197,8 +203,6 @@ pub const JsApi = struct {
pub const name = "Response";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Response.deinit);
};
pub const constructor = bridge.constructor(Response.init, .{});

View File

@@ -46,6 +46,10 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {
.query_string => |qs| break :blk try paramsFromString(arena, qs, &page.buf),
.form_data => |fd| break :blk try KeyValueList.copy(arena, fd._list),
.value => |js_val| {
// Order matters here; Array is also an Object.
if (js_val.isArray()) {
break :blk try paramsFromArray(arena, js_val.toArray());
}
if (js_val.isObject()) {
break :blk try KeyValueList.fromJsObject(arena, js_val.toObject(), null, page);
}
@@ -134,6 +138,37 @@ pub fn sort(self: *URLSearchParams) void {
}.cmp);
}
fn paramsFromArray(allocator: Allocator, array: js.Array) !KeyValueList {
const array_len = array.len();
if (array_len == 0) {
return .empty;
}
var params = KeyValueList.init();
try params.ensureTotalCapacity(allocator, array_len);
// TODO: Release `params` on error.
var i: u32 = 0;
while (i < array_len) : (i += 1) {
const item = try array.get(i);
if (!item.isArray()) return error.InvalidArgument;
const as_array = item.toArray();
// Need 2 items for KV.
if (as_array.len() != 2) return error.InvalidArgument;
const name_val = try as_array.get(0);
const value_val = try as_array.get(1);
params._entries.appendAssumeCapacity(.{
.name = try name_val.toSSOWithAlloc(allocator),
.value = try value_val.toSSOWithAlloc(allocator),
});
}
return params;
}
fn paramsFromString(allocator: Allocator, input_: []const u8, buf: []u8) !KeyValueList {
if (input_.len == 0) {
return .empty;

View File

@@ -17,11 +17,12 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const log = @import("../../../log.zig");
const HttpClient = @import("../../HttpClient.zig");
const net_http = @import("../../../network/http.zig");
const http = @import("../../../network/http.zig");
const URL = @import("../../URL.zig");
const Mime = @import("../../Mime.zig");
@@ -38,13 +39,15 @@ const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const XMLHttpRequest = @This();
_rc: lp.RC(u8) = .{},
_page: *Page,
_proto: *XMLHttpRequestEventTarget,
_arena: Allocator,
_transfer: ?*HttpClient.Transfer = null,
_active_request: bool = false,
_url: [:0]const u8 = "",
_method: net_http.Method = .GET,
_method: http.Method = .GET,
_request_headers: *Headers,
_request_body: ?[]const u8 = null,
@@ -87,21 +90,18 @@ const ResponseType = enum {
pub fn init(page: *Page) !*XMLHttpRequest {
const arena = try page.getArena(.{ .debug = "XMLHttpRequest" });
errdefer page.releaseArena(arena);
return page._factory.xhrEventTarget(arena, XMLHttpRequest{
const xhr = try page._factory.xhrEventTarget(arena, XMLHttpRequest{
._page = page,
._arena = arena,
._proto = undefined,
._request_headers = try Headers.init(null, page),
});
return xhr;
}
pub fn deinit(self: *XMLHttpRequest, shutdown: bool, session: *Session) void {
pub fn deinit(self: *XMLHttpRequest, session: *Session) void {
if (self._transfer) |transfer| {
if (shutdown) {
transfer.terminate();
} else {
transfer.abort(error.Abort);
}
transfer.abort(error.Abort);
self._transfer = null;
}
@@ -137,6 +137,22 @@ pub fn deinit(self: *XMLHttpRequest, shutdown: bool, session: *Session) void {
session.releaseArena(self._arena);
}
fn releaseSelfRef(self: *XMLHttpRequest) void {
if (self._active_request == false) {
return;
}
self.releaseRef(self._page._session);
self._active_request = false;
}
pub fn releaseRef(self: *XMLHttpRequest, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *XMLHttpRequest) void {
self._rc.acquire();
}
fn asEventTarget(self: *XMLHttpRequest) *EventTarget {
return self._proto._proto;
}
@@ -245,8 +261,8 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
.error_callback = httpErrorCallback,
.shutdown_callback = httpShutdownCallback,
});
page.js.strongRef(self);
self.acquireRef();
self._active_request = true;
}
fn handleBlobUrl(self: *XMLHttpRequest, page: *Page) !void {
@@ -390,7 +406,7 @@ fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
self._transfer = transfer;
}
fn httpHeaderCallback(transfer: *HttpClient.Transfer, header: net_http.Header) !void {
fn httpHeaderCallback(transfer: *HttpClient.Transfer, header: http.Header) !void {
const self: *XMLHttpRequest = @ptrCast(@alignCast(transfer.ctx));
const joined = try std.fmt.allocPrint(self._arena, "{s}: {s}", .{ header.name, header.value });
try self._response_headers.append(self._arena, joined);
@@ -486,15 +502,17 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
.loaded = loaded,
}, page);
page.js.weakRef(self);
self.releaseSelfRef();
}
fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *XMLHttpRequest = @ptrCast(@alignCast(ctx));
// http client will close it after an error, it isn't safe to keep around
self._transfer = null;
self.handleError(err);
self._page.js.weakRef(self);
if (self._transfer != null) {
self._transfer = null;
}
self.releaseSelfRef();
}
fn httpShutdownCallback(ctx: *anyopaque) void {
@@ -505,10 +523,10 @@ fn httpShutdownCallback(ctx: *anyopaque) void {
pub fn abort(self: *XMLHttpRequest) void {
self.handleError(error.Abort);
if (self._transfer) |transfer| {
transfer.abort(error.Abort);
self._transfer = null;
transfer.abort(error.Abort);
}
self._page.js.weakRef(self);
self.releaseSelfRef();
}
fn handleError(self: *XMLHttpRequest, err: anyerror) void {
@@ -556,7 +574,7 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, page: *Page) !void {
}
}
fn parseMethod(method: []const u8) !net_http.Method {
fn parseMethod(method: []const u8) !http.Method {
if (std.ascii.eqlIgnoreCase(method, "get")) {
return .GET;
}
@@ -582,8 +600,6 @@ pub const JsApi = struct {
pub const name = "XMLHttpRequest";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(XMLHttpRequest.deinit);
};
pub const constructor = bridge.constructor(XMLHttpRequest.init, .{});

View File

@@ -74,7 +74,7 @@ fn preprocessInput(arena: Allocator, input: []const u8) ![]const u8 {
return result.items;
}
pub fn parseList(arena: Allocator, input: []const u8, page: *Page) ParseError![]const Selector.Selector {
pub fn parseList(arena: Allocator, input: []const u8) ParseError![]const Selector.Selector {
// Preprocess input to normalize line endings
const preprocessed = try preprocessInput(arena, input);
@@ -140,7 +140,7 @@ pub fn parseList(arena: Allocator, input: []const u8, page: *Page) ParseError![]
const selector_input = std.mem.trimRight(u8, trimmed[0..comma_pos], &std.ascii.whitespace);
if (selector_input.len > 0) {
const selector = try parse(arena, selector_input, page);
const selector = try parse(arena, selector_input);
try selectors.append(arena, selector);
}
@@ -155,7 +155,7 @@ pub fn parseList(arena: Allocator, input: []const u8, page: *Page) ParseError![]
return selectors.items;
}
pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Selector.Selector {
pub fn parse(arena: Allocator, input: []const u8) ParseError!Selector.Selector {
var parser = Parser{ .input = input };
var segments: std.ArrayList(Segment) = .empty;
var current_compound: std.ArrayList(Part) = .empty;
@@ -164,7 +164,7 @@ pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Select
while (parser.skipSpaces()) {
if (parser.peek() == 0) break;
const part = try parser.parsePart(arena, page);
const part = try parser.parsePart(arena);
try current_compound.append(arena, part);
// Check what comes after this part
@@ -238,7 +238,7 @@ pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Select
while (parser.skipSpaces()) {
if (parser.peek() == 0) break;
const part = try parser.parsePart(arena, page);
const part = try parser.parsePart(arena);
try current_compound.append(arena, part);
// Check what comes after this part
@@ -289,7 +289,7 @@ pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Select
};
}
fn parsePart(self: *Parser, arena: Allocator, page: *Page) !Part {
fn parsePart(self: *Parser, arena: Allocator) !Part {
return switch (self.peek()) {
'#' => .{ .id = try self.id(arena) },
'.' => .{ .class = try self.class(arena) },
@@ -297,16 +297,17 @@ fn parsePart(self: *Parser, arena: Allocator, page: *Page) !Part {
self.input = self.input[1..];
break :blk .universal;
},
'[' => .{ .attribute = try self.attribute(arena, page) },
':' => .{ .pseudo_class = try self.pseudoClass(arena, page) },
'[' => .{ .attribute = try self.attribute(arena) },
':' => .{ .pseudo_class = try self.pseudoClass(arena) },
'a'...'z', 'A'...'Z', '_', '\\', 0x80...0xFF => blk: {
// Use parseIdentifier for full escape support
const tag_name = try self.parseIdentifier(arena, error.InvalidTagSelector);
if (tag_name.len > 256) {
return error.InvalidTagSelector;
}
var buf: [256]u8 = undefined;
// Try to match as a known tag enum for optimization
const lower = std.ascii.lowerString(&page.buf, tag_name);
const lower = std.ascii.lowerString(&buf, tag_name);
if (Node.Element.Tag.parseForMatch(lower)) |known_tag| {
break :blk .{ .tag = known_tag };
}
@@ -373,7 +374,7 @@ fn consumeUntilCommaOrParen(self: *Parser) []const u8 {
return result;
}
fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoClass {
fn pseudoClass(self: *Parser, arena: Allocator) !Selector.PseudoClass {
if (comptime IS_DEBUG) {
// Should have been verified by caller
std.debug.assert(self.peek() == ':');
@@ -445,7 +446,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
if (self.peek() == 0) return error.InvalidPseudoClass;
// Parse a full selector (with potential combinators and compounds)
const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);
const selector = try parse(arena, self.consumeUntilCommaOrParen());
try selectors.append(arena, selector);
_ = self.skipSpaces();
@@ -472,7 +473,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
if (self.peek() == ')') break;
if (self.peek() == 0) return error.InvalidPseudoClass;
const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);
const selector = try parse(arena, self.consumeUntilCommaOrParen());
try selectors.append(arena, selector);
_ = self.skipSpaces();
@@ -499,7 +500,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
if (self.peek() == ')') break;
if (self.peek() == 0) return error.InvalidPseudoClass;
const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);
const selector = try parse(arena, self.consumeUntilCommaOrParen());
try selectors.append(arena, selector);
_ = self.skipSpaces();
@@ -526,7 +527,7 @@ fn pseudoClass(self: *Parser, arena: Allocator, page: *Page) !Selector.PseudoCla
if (self.peek() == ')') break;
if (self.peek() == 0) return error.InvalidPseudoClass;
const selector = try parse(arena, self.consumeUntilCommaOrParen(), page);
const selector = try parse(arena, self.consumeUntilCommaOrParen());
try selectors.append(arena, selector);
_ = self.skipSpaces();
@@ -898,7 +899,7 @@ fn tag(self: *Parser) ![]const u8 {
return input[0..i];
}
fn attribute(self: *Parser, arena: Allocator, page: *Page) !Selector.Attribute {
fn attribute(self: *Parser, arena: Allocator) !Selector.Attribute {
if (comptime IS_DEBUG) {
// should have been verified by caller
std.debug.assert(self.peek() == '[');
@@ -910,8 +911,7 @@ fn attribute(self: *Parser, arena: Allocator, page: *Page) !Selector.Attribute {
const attr_name = try self.attributeName();
// Normalize the name to lowercase for fast matching (consistent with Attribute.normalizeNameForLookup)
const normalized = try Attribute.normalizeNameForLookup(.wrap(attr_name), page);
const name = try normalized.dupe(arena);
const name = try Attribute.normalizeNameForLookupAlloc(arena, .wrap(attr_name));
var case_insensitive = false;
_ = self.skipSpaces();

View File

@@ -20,41 +20,24 @@ const std = @import("std");
const String = @import("../../../string.zig").String;
const Parser = @import("Parser.zig");
const Node = @import("../Node.zig");
const Page = @import("../../Page.zig");
const Parser = @import("Parser.zig");
pub const List = @import("List.zig");
pub fn querySelector(root: *Node, input: []const u8, page: *Page) !?*Node.Element {
const Allocator = std.mem.Allocator;
pub fn parseLeaky(arena: Allocator, input: []const u8) !Parsed {
if (input.len == 0) {
return error.SyntaxError;
}
return .{ .selectors = try Parser.parseList(arena, input) };
}
const arena = page.call_arena;
const selectors = try Parser.parseList(arena, input, page);
for (selectors) |selector| {
// Fast path: single compound with only an ID selector
if (selector.segments.len == 0 and selector.first.parts.len == 1) {
const first = selector.first.parts[0];
if (first == .id) {
const el = page.getElementByIdFromNode(root, first.id) orelse continue;
// Check if the element is within the root subtree
const node = el.asNode();
if (node != root and root.contains(node)) {
return el;
}
continue;
}
}
if (List.initOne(root, selector, page)) |node| {
if (node.is(Node.Element)) |el| {
return el;
}
}
}
return null;
pub fn querySelector(root: *Node, input: []const u8, page: *Page) !?*Node.Element {
const parsed = try parseLeaky(page.call_arena, input);
return parsed.query(root, page);
}
pub fn querySelectorAll(root: *Node, input: []const u8, page: *Page) !*List {
@@ -67,7 +50,7 @@ pub fn querySelectorAll(root: *Node, input: []const u8, page: *Page) !*List {
var nodes: std.AutoArrayHashMapUnmanaged(*Node, void) = .empty;
const selectors = try Parser.parseList(arena, input, page);
const selectors = try Parser.parseList(arena, input);
for (selectors) |selector| {
try List.collect(arena, root, selector, &nodes, page);
}
@@ -86,7 +69,7 @@ pub fn matches(el: *Node.Element, input: []const u8, page: *Page) !bool {
}
const arena = page.call_arena;
const selectors = try Parser.parseList(arena, input, page);
const selectors = try Parser.parseList(arena, input);
for (selectors) |selector| {
if (List.matches(el.asNode(), selector, el.asNode(), page)) {
@@ -104,7 +87,7 @@ pub fn matchesWithScope(el: *Node.Element, input: []const u8, scope: *Node.Eleme
}
const arena = page.call_arena;
const selectors = try Parser.parseList(arena, input, page);
const selectors = try Parser.parseList(arena, input);
for (selectors) |selector| {
if (List.matches(el.asNode(), selector, scope.asNode(), page)) {
@@ -292,3 +275,32 @@ pub const Selector = struct {
}
}
};
pub const Parsed = struct {
selectors: []const Selector,
pub fn query(self: Parsed, root: *Node, page: *Page) !?*Node.Element {
for (self.selectors) |selector| {
// Fast path: single compound with only an ID selector
if (selector.segments.len == 0 and selector.first.parts.len == 1) {
const first = selector.first.parts[0];
if (first == .id) {
const el = page.getElementByIdFromNode(root, first.id) orelse continue;
// Check if the element is within the root subtree
const node = el.asNode();
if (node != root and root.contains(node)) {
return el;
}
continue;
}
}
if (List.initOne(root, selector, page)) |node| {
if (node.is(Node.Element)) |el| {
return el;
}
}
}
return null;
}
};

View File

@@ -1179,7 +1179,7 @@ test "AXNode: writer" {
var registry = Node.Registry.init(testing.allocator);
defer registry.deinit();
var page = try testing.pageTest("cdp/dom3.html");
var page = try testing.pageTest("cdp/dom3.html", .{});
defer page._session.removePage();
var doc = page.window._document;

File diff suppressed because it is too large Load Diff

View File

@@ -343,7 +343,7 @@ test "cdp Node: Registry register" {
try testing.expectEqual(0, registry.lookup_by_id.count());
try testing.expectEqual(0, registry.lookup_by_node.count());
var page = try testing.pageTest("cdp/registry1.html");
var page = try testing.pageTest("cdp/registry1.html", .{});
defer page._session.removePage();
var doc = page.window._document;
@@ -400,7 +400,7 @@ test "cdp Node: search list" {
}
{
var page = try testing.pageTest("cdp/registry2.html");
var page = try testing.pageTest("cdp/registry2.html", .{});
defer page._session.removePage();
var doc = page.window._document;
@@ -440,7 +440,7 @@ test "cdp Node: Writer" {
var registry = Registry.init(testing.allocator);
defer registry.deinit();
var page = try testing.pageTest("cdp/registry3.html");
var page = try testing.pageTest("cdp/registry3.html", .{});
defer page._session.removePage();
var doc = page.window._document;

View File

@@ -18,8 +18,9 @@
const std = @import("std");
const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: anytype) !void {
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum {
enable,
disable,
@@ -32,15 +33,15 @@ pub fn processMessage(cmd: anytype) !void {
.getFullAXTree => return getFullAXTree(cmd),
}
}
fn enable(cmd: anytype) !void {
fn enable(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
fn disable(cmd: anytype) !void {
fn disable(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
fn getFullAXTree(cmd: anytype) !void {
fn getFullAXTree(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
depth: ?i32 = null,
frameId: ?[]const u8 = null,

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
// TODO: hard coded data
const PROTOCOL_VERSION = "1.3";
@@ -35,7 +36,7 @@ const PRODUCT = "Chrome/124.0.6367.29";
const JS_VERSION = "12.4.254.8";
const DEV_TOOLS_WINDOW_ID = 1923710101;
pub fn processMessage(cmd: anytype) !void {
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum {
getVersion,
setPermission,
@@ -57,7 +58,7 @@ pub fn processMessage(cmd: anytype) !void {
}
}
fn getVersion(cmd: anytype) !void {
fn getVersion(cmd: *CDP.Command) !void {
// TODO: pre-serialize?
return cmd.sendResult(.{
.protocolVersion = PROTOCOL_VERSION,
@@ -69,7 +70,7 @@ fn getVersion(cmd: anytype) !void {
}
// TODO: noop method
fn setDownloadBehavior(cmd: anytype) !void {
fn setDownloadBehavior(cmd: *CDP.Command) !void {
// const params = (try cmd.params(struct {
// behavior: []const u8,
// browserContextId: ?[]const u8 = null,
@@ -80,7 +81,7 @@ fn setDownloadBehavior(cmd: anytype) !void {
return cmd.sendResult(null, .{ .include_session_id = false });
}
fn getWindowForTarget(cmd: anytype) !void {
fn getWindowForTarget(cmd: *CDP.Command) !void {
// const params = (try cmd.params(struct {
// targetId: ?[]const u8 = null,
// })) orelse return error.InvalidParams;
@@ -91,22 +92,22 @@ fn getWindowForTarget(cmd: anytype) !void {
}
// TODO: noop method
fn setWindowBounds(cmd: anytype) !void {
fn setWindowBounds(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn grantPermissions(cmd: anytype) !void {
fn grantPermissions(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn setPermission(cmd: anytype) !void {
fn setPermission(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn resetPermissions(cmd: anytype) !void {
fn resetPermissions(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}

View File

@@ -17,8 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: anytype) !void {
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum {
enable,
}, cmd.input.action) orelse return error.UnknownMethod;

View File

@@ -18,17 +18,18 @@
const std = @import("std");
const id = @import("../id.zig");
const log = @import("../../log.zig");
const CDP = @import("../CDP.zig");
const Node = @import("../Node.zig");
const log = @import("../../log.zig");
const dump = @import("../../browser/dump.zig");
const js = @import("../../browser/js/js.zig");
const DOMNode = @import("../../browser/webapi/Node.zig");
const Selector = @import("../../browser/webapi/selector/Selector.zig");
const dump = @import("../../browser/dump.zig");
const js = @import("../../browser/js/js.zig");
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: anytype) !void {
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum {
enable,
getDocument,
@@ -69,7 +70,7 @@ pub fn processMessage(cmd: anytype) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument
fn getDocument(cmd: anytype) !void {
fn getDocument(cmd: *CDP.Command) !void {
const Params = struct {
// CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome
depth: i32 = 3,
@@ -89,7 +90,7 @@ fn getDocument(cmd: anytype) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
fn performSearch(cmd: anytype) !void {
fn performSearch(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
query: []const u8,
includeUserAgentShadowDOM: ?bool = null,
@@ -116,7 +117,7 @@ fn performSearch(cmd: anytype) !void {
// hierarchy of each nodes.
// We dispatch event in the reverse order: from the top level to the direct parents.
// We should dispatch a node only if it has never been sent.
fn dispatchSetChildNodes(cmd: anytype, dom_nodes: []const *DOMNode) !void {
fn dispatchSetChildNodes(cmd: *CDP.Command, dom_nodes: []const *DOMNode) !void {
const arena = cmd.arena;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const session_id = bc.session_id orelse return error.SessionIdNotLoaded;
@@ -172,7 +173,7 @@ fn dispatchSetChildNodes(cmd: anytype, dom_nodes: []const *DOMNode) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults
fn discardSearchResults(cmd: anytype) !void {
fn discardSearchResults(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
searchId: []const u8,
})) orelse return error.InvalidParams;
@@ -184,7 +185,7 @@ fn discardSearchResults(cmd: anytype) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults
fn getSearchResults(cmd: anytype) !void {
fn getSearchResults(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
searchId: []const u8,
fromIndex: u32,
@@ -209,7 +210,7 @@ fn getSearchResults(cmd: anytype) !void {
return cmd.sendResult(.{ .nodeIds = node_ids[params.fromIndex..params.toIndex] }, .{});
}
fn querySelector(cmd: anytype) !void {
fn querySelector(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
nodeId: Node.Id,
selector: []const u8,
@@ -235,7 +236,7 @@ fn querySelector(cmd: anytype) !void {
}, .{});
}
fn querySelectorAll(cmd: anytype) !void {
fn querySelectorAll(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
nodeId: Node.Id,
selector: []const u8,
@@ -266,7 +267,7 @@ fn querySelectorAll(cmd: anytype) !void {
}, .{});
}
fn resolveNode(cmd: anytype) !void {
fn resolveNode(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?u32 = null,
@@ -327,7 +328,7 @@ fn resolveNode(cmd: anytype) !void {
} }, .{});
}
fn describeNode(cmd: anytype) !void {
fn describeNode(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
@@ -374,7 +375,7 @@ fn rectToQuad(rect: DOMNode.Element.DOMRect) Quad {
};
}
fn scrollIntoViewIfNeeded(cmd: anytype) !void {
fn scrollIntoViewIfNeeded(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?u32 = null,
@@ -397,7 +398,7 @@ fn scrollIntoViewIfNeeded(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
fn getNode(arena: Allocator, bc: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node {
fn getNode(arena: Allocator, bc: *CDP.BrowserContext, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node {
const input_node_id = node_id orelse backend_node_id;
if (input_node_id) |input_node_id_| {
return bc.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound;
@@ -417,7 +418,7 @@ fn getNode(arena: Allocator, bc: anytype, node_id: ?Node.Id, backend_node_id: ?N
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads
// Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface
fn getContentQuads(cmd: anytype) !void {
fn getContentQuads(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
@@ -443,7 +444,7 @@ fn getContentQuads(cmd: anytype) !void {
return cmd.sendResult(.{ .quads = &.{quad} }, .{});
}
fn getBoxModel(cmd: anytype) !void {
fn getBoxModel(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?u32 = null,
@@ -472,7 +473,7 @@ fn getBoxModel(cmd: anytype) !void {
} }, .{});
}
fn requestChildNodes(cmd: anytype) !void {
fn requestChildNodes(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
nodeId: Node.Id,
depth: i32 = 1,
@@ -496,7 +497,7 @@ fn requestChildNodes(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
fn getFrameOwner(cmd: anytype) !void {
fn getFrameOwner(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
frameId: []const u8,
})) orelse return error.InvalidParams;
@@ -512,7 +513,7 @@ fn getFrameOwner(cmd: anytype) !void {
return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{});
}
fn getOuterHTML(cmd: anytype) !void {
fn getOuterHTML(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
@@ -534,7 +535,7 @@ fn getOuterHTML(cmd: anytype) !void {
return cmd.sendResult(.{ .outerHTML = aw.written() }, .{});
}
fn requestNode(cmd: anytype) !void {
fn requestNode(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
objectId: []const u8,
})) orelse return error.InvalidParams;

View File

@@ -17,9 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
const log = @import("../../log.zig");
pub fn processMessage(cmd: anytype) !void {
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum {
setEmulatedMedia,
setFocusEmulationEnabled,
@@ -38,7 +39,7 @@ pub fn processMessage(cmd: anytype) !void {
}
// TODO: noop method
fn setEmulatedMedia(cmd: anytype) !void {
fn setEmulatedMedia(cmd: *CDP.Command) !void {
// const input = (try const incoming.params(struct {
// media: ?[]const u8 = null,
// features: ?[]struct{
@@ -51,7 +52,7 @@ fn setEmulatedMedia(cmd: anytype) !void {
}
// TODO: noop method
fn setFocusEmulationEnabled(cmd: anytype) !void {
fn setFocusEmulationEnabled(cmd: *CDP.Command) !void {
// const input = (try const incoming.params(struct {
// enabled: bool,
// })) orelse return error.InvalidParams;
@@ -59,16 +60,16 @@ fn setFocusEmulationEnabled(cmd: anytype) !void {
}
// TODO: noop method
fn setDeviceMetricsOverride(cmd: anytype) !void {
fn setDeviceMetricsOverride(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn setTouchEmulationEnabled(cmd: anytype) !void {
fn setTouchEmulationEnabled(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
fn setUserAgentOverride(cmd: anytype) !void {
fn setUserAgentOverride(cmd: *CDP.Command) !void {
log.info(.app, "setUserAgentOverride ignored", .{});
return cmd.sendResult(null, .{});
}

View File

@@ -17,17 +17,19 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
const log = @import("../../log.zig");
const network = @import("network.zig");
const HttpClient = @import("../../browser/HttpClient.zig");
const net_http = @import("../../network/http.zig");
const http = @import("../../network/http.zig");
const Notification = @import("../../Notification.zig");
pub fn processMessage(cmd: anytype) !void {
const network = @import("network.zig");
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum {
disable,
enable,
@@ -135,13 +137,13 @@ const ErrorReason = enum {
BlockedByResponse,
};
fn disable(cmd: anytype) !void {
fn disable(cmd: *CDP.Command) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
bc.fetchDisable();
return cmd.sendResult(null, .{});
}
fn enable(cmd: anytype) !void {
fn enable(cmd: *CDP.Command) !void {
const params = (try cmd.params(EnableParam)) orelse EnableParam{};
if (!arePatternsSupported(params.patterns)) {
log.warn(.not_implemented, "Fetch.enable", .{ .params = "pattern" });
@@ -180,7 +182,7 @@ fn arePatternsSupported(patterns: []RequestPattern) bool {
return true;
}
pub fn requestIntercept(bc: anytype, intercept: *const Notification.RequestIntercept) !void {
pub fn requestIntercept(bc: *CDP.BrowserContext, intercept: *const Notification.RequestIntercept) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
@@ -215,14 +217,14 @@ pub fn requestIntercept(bc: anytype, intercept: *const Notification.RequestInter
intercept.wait_for_interception.* = true;
}
fn continueRequest(cmd: anytype) !void {
fn continueRequest(cmd: *CDP.Command) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
requestId: []const u8, // INT-{d}"
url: ?[]const u8 = null,
method: ?[]const u8 = null,
postData: ?[]const u8 = null,
headers: ?[]const net_http.Header = null,
headers: ?[]const http.Header = null,
interceptResponse: bool = false,
})) orelse return error.InvalidParams;
@@ -247,7 +249,7 @@ fn continueRequest(cmd: anytype) !void {
try transfer.updateURL(try arena.dupeZ(u8, url));
}
if (params.method) |method| {
transfer.req.method = std.meta.stringToEnum(net_http.Method, method) orelse return error.InvalidParams;
transfer.req.method = std.meta.stringToEnum(http.Method, method) orelse return error.InvalidParams;
}
if (params.headers) |headers| {
@@ -275,7 +277,7 @@ const AuthChallengeResponse = enum {
ProvideCredentials,
};
fn continueWithAuth(cmd: anytype) !void {
fn continueWithAuth(cmd: *CDP.Command) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
requestId: []const u8, // "INT-{d}"
@@ -318,13 +320,13 @@ fn continueWithAuth(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
fn fulfillRequest(cmd: anytype) !void {
fn fulfillRequest(cmd: *CDP.Command) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
requestId: []const u8, // "INT-{d}"
responseCode: u16,
responseHeaders: ?[]const net_http.Header = null,
responseHeaders: ?[]const http.Header = null,
binaryResponseHeaders: ?[]const u8 = null,
body: ?[]const u8 = null,
responsePhrase: ?[]const u8 = null,
@@ -360,7 +362,7 @@ fn fulfillRequest(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
fn failRequest(cmd: anytype) !void {
fn failRequest(cmd: *CDP.Command) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
requestId: []const u8, // "INT-{d}"
@@ -382,7 +384,7 @@ fn failRequest(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
pub fn requestAuthRequired(bc: anytype, intercept: *const Notification.RequestAuthRequired) !void {
pub fn requestAuthRequired(bc: *CDP.BrowserContext, intercept: *const Notification.RequestAuthRequired) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;

View File

@@ -17,8 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: anytype) !void {
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum {
dispatchKeyEvent,
dispatchMouseEvent,
@@ -33,7 +34,7 @@ pub fn processMessage(cmd: anytype) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
fn dispatchKeyEvent(cmd: anytype) !void {
fn dispatchKeyEvent(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
type: Type,
key: []const u8 = "",
@@ -74,7 +75,7 @@ fn dispatchKeyEvent(cmd: anytype) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
fn dispatchMouseEvent(cmd: anytype) !void {
fn dispatchMouseEvent(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
x: f64,
y: f64,
@@ -104,7 +105,7 @@ fn dispatchMouseEvent(cmd: anytype) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText
fn insertText(cmd: anytype) !void {
fn insertText(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
text: []const u8, // The text to insert
})) orelse return error.InvalidParams;

View File

@@ -17,8 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: anytype) !void {
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum {
enable,
disable,

View File

@@ -17,8 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: anytype) !void {
pub fn processMessage(cmd: *CDP.Command) !void {
const action = std.meta.stringToEnum(enum {
enable,
disable,

Some files were not shown because too many files have changed in this diff Show More