Compare commits

...

89 Commits

Author SHA1 Message Date
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
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
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
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
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
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
Pierre Tachoire
03ed45637a Merge pull request #1889 from lightpanda-io/wp/mrdimidium/refactor-redirects
Some checks failed
e2e-test / zig build release (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
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 / 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
Rework header/data callbacks in HttpClient
2026-03-27 14:22:58 +01:00
Nikolay Govorov
9068fe718e Fix SameSite cookies 2026-03-27 11:16:46 +00:00
Nikolay Govorov
5369d25213 fix recv e2e test 2026-03-27 09:49:16 +00:00
Nikolay Govorov
649d8d1024 Remove duplication in cookies instalation 2026-03-27 09:49:13 +00:00
Nikolay Govorov
15d60d845a Fixup error handling in HttpClient process messages 2026-03-27 09:49:11 +00:00
Nikolay Govorov
c4b837b598 Revert log reimport 2026-03-27 09:49:09 +00:00
Nikolay Govorov
54391238c9 Move cdp callbacks from dataCallback to processMessages 2026-03-27 09:49:07 +00:00
Nikolay Govorov
d33edc5697 Fixup cookies management 2026-03-27 09:49:05 +00:00
Nikolay Govorov
16ca8d4b14 Fix cleanup connections in HttpClient 2026-03-27 09:49:03 +00:00
Nikolay Govorov
707ffb4893 Move redirects handling from curl callbacks 2026-03-27 09:48:59 +00:00
Pierre Tachoire
4782b37216 Merge pull request #2016 from lightpanda-io/readme-mention-cors
mention CORS is missing in the README's status
2026-03-27 08:34:09 +01:00
Pierre Tachoire
ce197256dd Merge pull request #2010 from lightpanda-io/build-pre-nightly
build: simplify nightly versioning
2026-03-27 08:33:45 +01:00
Pierre Tachoire
e6d644998a mention CORS is missing in the README's status 2026-03-27 08:26:56 +01:00
Karl Seguin
67bd555e75 Merge pull request #2013 from lightpanda-io/cleanup_dead_code_removal
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
Remove unused imports
2026-03-27 13:52:49 +08:00
Adrià Arrufat
a10e533701 Remove more unused imports 2026-03-27 14:24:17 +09:00
Karl Seguin
0065677273 Merge pull request #2011 from lightpanda-io/mcp-fixes
MCP fixes
2026-03-27 13:02:59 +08:00
Karl Seguin
226d9bfc6f zig fmt 2026-03-27 12:47:24 +08:00
Karl Seguin
2e65ae632e Merge pull request #2009 from lightpanda-io/fix/issue-1960
mcp: improve argument parsing error handling
2026-03-27 12:46:34 +08:00
Karl Seguin
ea422075c7 Remove unused imports
And some smaller cleanups.
2026-03-27 12:45:26 +08:00
Adrià Arrufat
1d54e6944b mcp: send error response when message is too long 2026-03-27 11:36:18 +09:00
Adrià Arrufat
de32e5cf34 mcp: handle missing request IDs safely 2026-03-27 11:34:06 +09:00
Adrià Arrufat
c8d8ca5e94 mcp: improve error handling in resources and tools
- Handle failures during HTML, Markdown, and link serialization.
- Return MCP internal errors when result serialization fails.
- Refactor resource reading logic for better clarity and consistency.
2026-03-27 11:28:47 +09:00
Adrià Arrufat
7f2139f612 build: simplify nightly versioning 2026-03-27 10:47:43 +09:00
Adrià Arrufat
da0828620f mcp: improve argument parsing error handling
Closes #1960
2026-03-27 10:04:45 +09:00
Adrià Arrufat
cdd33621e3 Merge pull request #2005 from lightpanda-io/mcp-lp-node-registry
MCP/CDP: unify node registration
2026-03-27 09:36:08 +09:00
Karl Seguin
8001709506 Merge pull request #2002 from lightpanda-io/nikneym/form-data-event
Support `FormDataEvent`
2026-03-27 08:16:32 +08:00
Karl Seguin
a0ae6b4c92 Merge pull request #2008 from buley/feature/fix-scanner-warnings
chore: fix dead code and error swallowing warnings
2026-03-27 08:10:31 +08:00
Karl Seguin
fdf7f5267a Merge pull request #2001 from lightpanda-io/refactor/mcp-tools-dedup
mcp: extract parseOptionalAndGetPage helper
2026-03-27 07:58:18 +08: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
Taylor
88e0b39d6b chore: fix dead code and error swallowing warnings
Fixes issues reported by polyglot-scanner:
- Removed explicit `return` keywords and trailing semicolons to resolve DEAD_CODE/DEAD_BRANCH warnings.
- Replaced `epoch::advance().unwrap()` and `stats::resident::read().unwrap()` with safer alternatives (`drop` and `unwrap_or(0)`) to resolve ERROR_SWALLOW warnings.
- Replaced `let _ = Box::from_raw(...)` with `drop(Box::from_raw(...))` to correctly drop the box while fixing the ERROR_SWALLOW warning.
2026-03-26 09:58:49 -07:00
Pierre Tachoire
f95396a487 Merge pull request #1998 from lightpanda-io/url_origin_fix
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 authority parsing
2026-03-26 17:32:40 +01:00
Pierre Tachoire
d02d05b246 Merge pull request #2004 from lightpanda-io/nikneym/resize-unobserver
`ResizeObserver`: make `unobserve` available in JS context
2026-03-26 16:48:42 +01:00
Pierre Tachoire
7b2d817d0e Merge pull request #2003 from lightpanda-io/nikneym/canvas-access-canvas
`CanvasRenderingContext2D`: make canvas able to access canvas element
2026-03-26 16:48:11 +01:00
Adrià Arrufat
7e778a17d6 MCP/CDP: unify node registration
This fixes a bug in MCP where interactive elements were not assigned
a backendNodeId, preventing agents from clicking or filling them. Also
extracts link collection to a shared browser module.
2026-03-26 23:51:43 +09:00
Halil Durak
d447d1e3c7 ResizeObserver: make unobserve available in JS context 2026-03-26 16:37:17 +03:00
Halil Durak
8684d35394 add tests 2026-03-26 16:35:23 +03:00
Halil Durak
e243f96988 CanvasRenderingContext2D: make canvas able to access canvas element 2026-03-26 16:35:13 +03:00
Halil Durak
5e6082b5e9 FormDataEvent: add tests 2026-03-26 14:04:03 +03:00
Halil Durak
1befd9a5e8 make comment on SubmitEvent doc-comment 2026-03-26 14:03:51 +03:00
Halil Durak
e103ce0f39 FormDataEvent: initial support 2026-03-26 14:03:33 +03:00
Adrià Arrufat
14fa2da2ad mcp: remove duplicate code in testLoadPage 2026-03-26 19:57:14 +09:00
Adrià Arrufat
96d24b5dc6 mcp: extract parseOptionalAndGetPage helper
Deduplicate the repeated "parse optional URL, maybe navigate, get page"
pattern across 6 MCP tool handlers (markdown, links, semantic_tree,
interactiveElements, structuredData, detectForms).
2026-03-26 19:44:44 +09:00
Karl Seguin
0588cc374d Improve authority parsing
Only look for @ within the first part of the url (up to the first possible
separator, i.e /, # or ?). This fixes potentially incorrect (and insecure)
getOrigin and getHost, both of which use the new helper.

Also make port parsing IPv6-aware.
2026-03-26 13:22:56 +08:00
132 changed files with 3181 additions and 2199 deletions

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

@@ -7,7 +7,7 @@ env:
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dversion_string={0}', github.ref_name) || format('-Dpre_version={0}', 'nightly') }}
VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dversion={0}', github.ref_name) || '-Dversion=nightly' }}
on:
push:

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:**
@@ -170,6 +170,7 @@ You may still encounter errors or crashes. Please open an issue with specifics i
Here are the key features we have implemented:
- [ ] CORS [#2015](https://github.com/lightpanda-io/browser/issues/2015)
- [x] HTTP loader ([Libcurl](https://curl.se/libcurl/))
- [x] HTML parser ([html5ever](https://github.com/servo/html5ever))
- [x] DOM tree

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

@@ -719,39 +719,45 @@ fn buildCurl(
return lib;
}
/// Returns `MAJOR.MINOR.PATCH-dev` when `git describe` fails.
/// Resolves the semantic version of the build.
///
/// The base version is read from `build.zig.zon`. This can be overridden
/// using the `-Dversion` command-line flag:
/// - If the flag contains a full semantic version (e.g., `1.2.3`), it replaces
/// the base version entirely.
/// - If the flag contains a simple string (e.g., `nightly`), it replaces only
/// the pre-release tag of the base version (e.g., `1.0.0-dev` -> `1.0.0-nightly`).
///
/// For versions that have a pre-release tag and no explicit build metadata,
/// this function automatically enriches the version with the git commit count
/// and short hash (e.g., `1.0.0-dev.5243+dbe45229`).
fn resolveVersion(b: *std.Build) std.SemanticVersion {
const version_string = b.option([]const u8, "version_string", "Override the version of this build");
if (version_string) |semver_string| {
return std.SemanticVersion.parse(semver_string) catch |err| {
std.debug.panic("Expected -Dversion-string={s} to be a semantic version: {}", .{ semver_string, err });
};
}
const opt_version = b.option([]const u8, "version", "Override the version of this build");
const pre_version = b.option([]const u8, "pre_version", "Override the pre version of this build");
const pre = blk: {
if (pre_version) |pre| {
break :blk pre;
const version = if (opt_version) |v|
std.SemanticVersion.parse(v) catch blk: {
var fallback = lightpanda_version;
fallback.pre = v;
break :blk fallback;
}
else
lightpanda_version;
break :blk lightpanda_version.pre;
};
// If it's a stable release (no pre or build metadata in build.zig.zon), use it as is
if (pre == null and lightpanda_version.build == null) return lightpanda_version;
// Only enrich versions that have a pre-release field and no explicit build metadata.
if (version.pre == null or version.build != null) return version;
// For dev/nightly versions, calculate the commit count and hash
const git_hash_raw = runGit(b, &.{ "rev-parse", "--short", "HEAD" }) catch return lightpanda_version;
const git_hash_raw = runGit(b, &.{ "rev-parse", "--short", "HEAD" }) catch return version;
const commit_hash = std.mem.trim(u8, git_hash_raw, " \n\r");
const git_count_raw = runGit(b, &.{ "rev-list", "--count", "HEAD" }) catch return lightpanda_version;
const git_count_raw = runGit(b, &.{ "rev-list", "--count", "HEAD" }) catch return version;
const commit_count = std.mem.trim(u8, git_count_raw, " \n\r");
return .{
.major = lightpanda_version.major,
.minor = lightpanda_version.minor,
.patch = lightpanda_version.patch,
.pre = b.fmt("{s}.{s}", .{ pre.?, commit_count }),
.major = version.major,
.minor = version.minor,
.patch = version.patch,
.pre = b.fmt("{s}.{s}", .{ version.pre.?, commit_count }),
.build = commit_hash,
};
}

View File

@@ -18,6 +18,7 @@
const std = @import("std");
const builtin = @import("builtin");
const log = @import("log.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
@@ -62,7 +63,7 @@ pub fn deinit(self: *ArenaPool) void {
var it = self._leak_track.iterator();
while (it.next()) |kv| {
if (kv.value_ptr.* != 0) {
std.debug.print("ArenaPool leak detected: '{s}' count={d}\n", .{ kv.key_ptr.*, kv.value_ptr.* });
log.err(.bug, "ArenaPool leak", .{ .name = kv.key_ptr.*, .count = kv.value_ptr.* });
has_leaks = true;
}
}
@@ -129,11 +130,11 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void {
if (self._leak_track.getPtr(entry.debug)) |count| {
count.* -= 1;
if (count.* < 0) {
std.debug.print("ArenaPool double-free detected: '{s}'\n", .{entry.debug});
log.err(.bug, "ArenaPool double-free", .{ .name = entry.debug });
@panic("ArenaPool: double-free detected");
}
} else {
std.debug.print("ArenaPool release of untracked arena: '{s}'\n", .{entry.debug});
log.err(.bug, "ArenaPool release unknown", .{ .name = entry.debug });
@panic("ArenaPool: release of untracked arena");
}
}

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,
@@ -222,6 +223,7 @@ pub const Serve = struct {
pub const Mcp = struct {
common: Common = .{},
version: mcp.Version = .default,
};
pub const DumpFormat = enum {
@@ -453,6 +455,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 +648,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 +671,7 @@ fn parseMcpArgs(
return error.UnkownOption;
}
return mcp;
return result;
}
fn parseFetchArgs(

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,6 +541,182 @@ 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" {

View File

@@ -22,7 +22,6 @@ const net = std.net;
const posix = std.posix;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const log = @import("log.zig");
const App = @import("App.zig");

View File

@@ -19,17 +19,13 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const js = @import("js/js.zig");
const log = @import("../log.zig");
const App = @import("../App.zig");
const HttpClient = @import("HttpClient.zig");
const ArenaPool = App.ArenaPool;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Session = @import("Session.zig");
const Notification = @import("../Notification.zig");

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 });
@@ -425,7 +425,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
ls.deinit();
}
const activation_state = ActivationState.create(event, target, page);
const activation_state = try ActivationState.create(event, target, page);
// Defer runs even on early return - ensures event phase is reset
// and default actions execute (unless prevented)
@@ -820,7 +820,7 @@ const ActivationState = struct {
const Input = Element.Html.Input;
fn create(event: *const Event, target: *Node, page: *Page) ?ActivationState {
fn create(event: *const Event, target: *Node, page: *Page) !?ActivationState {
if (event._type_string.eql(comptime .wrap("click")) == false) {
return null;
}

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)),

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,6 @@ const IS_DEBUG = builtin.mode == .Debug;
const log = @import("../log.zig");
const App = @import("../App.zig");
const String = @import("../string.zig").String;
const Mime = @import("Mime.zig");
@@ -43,7 +42,6 @@ const URL = @import("URL.zig");
const Blob = @import("webapi/Blob.zig");
const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig");
const EventTarget = @import("webapi/EventTarget.zig");
const CData = @import("webapi/CData.zig");
const Element = @import("webapi/Element.zig");
const HtmlElement = @import("webapi/element/Html.zig");
@@ -59,7 +57,6 @@ const AbstractRange = @import("webapi/AbstractRange.zig");
const MutationObserver = @import("webapi/MutationObserver.zig");
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
const storage = @import("webapi/storage/storage.zig");
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
const SubmitEvent = @import("webapi/event/SubmitEvent.zig");
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
@@ -67,7 +64,6 @@ const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
const MouseEvent = @import("webapi/event/MouseEvent.zig");
const HttpClient = @import("HttpClient.zig");
const ArenaPool = App.ArenaPool;
const timestamp = @import("../datetime.zig").timestamp;
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
@@ -385,12 +381,9 @@ pub fn getTitle(self: *Page) !?[]const u8 {
return null;
}
// Add comon headers for a request:
// * cookies
// Add common headers for a request:
// * referer
pub fn headersForRequest(self: *Page, temp: Allocator, url: [:0]const u8, headers: *HttpClient.Headers) !void {
try self.requestCookie(.{}).headersForRequest(temp, url, headers);
pub fn headersForRequest(self: *Page, headers: *HttpClient.Headers) !void {
// Build the referer
const referer = blk: {
if (self.referer_header == null) {
@@ -494,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,
@@ -526,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;
}
@@ -545,8 +539,6 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
if (opts.header) |hdr| {
try headers.add(hdr);
}
try self.requestCookie(.{ .is_navigation = true }).headersForRequest(self.arena, self.url, &headers);
// We dispatch page_navigate event before sending the request.
// It ensures the event page_navigated is not dispatched before this one.
session.notification.dispatch(.page_navigate, &.{
@@ -573,6 +565,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
.headers = headers,
.body = opts.body,
.cookie_jar = &session.cookie_jar,
.cookie_origin = self.url,
.resource_type = .document,
.notification = self._session.notification,
.header_callback = pageHeaderDoneCallback,
@@ -746,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 {
@@ -804,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 {
@@ -835,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" });
@@ -887,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;
}
@@ -1036,6 +1041,7 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
});
parser.parse(html);
self._parse_state = .complete;
self.documentIsComplete();
},
else => unreachable,
@@ -3399,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;
};
@@ -3495,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
@@ -3554,19 +3560,6 @@ pub fn insertText(self: *Page, v: []const u8) !void {
}
}
const RequestCookieOpts = struct {
is_http: bool = true,
is_navigation: bool = false,
};
pub fn requestCookie(self: *const Page, opts: RequestCookieOpts) HttpClient.RequestCookie {
return .{
.jar = &self._session.cookie_jar,
.origin = self.url,
.is_http = opts.is_http,
.is_navigation = opts.is_navigation,
};
}
fn asUint(comptime string: anytype) std.meta.Int(
.unsigned,
@bitSizeOf(@TypeOf(string.*)) - 8, // (- 8) to exclude sentinel 0

View File

@@ -21,12 +21,9 @@ const lp = @import("lightpanda");
const builtin = @import("builtin");
const log = @import("../log.zig");
const App = @import("../App.zig");
const Page = @import("Page.zig");
const Session = @import("Session.zig");
const Browser = @import("Browser.zig");
const Factory = @import("Factory.zig");
const HttpClient = @import("HttpClient.zig");
const IS_DEBUG = builtin.mode == .Debug;

View File

@@ -28,12 +28,10 @@ const String = @import("../string.zig").String;
const js = @import("js/js.zig");
const URL = @import("URL.zig");
const Page = @import("Page.zig");
const Browser = @import("Browser.zig");
const Element = @import("webapi/Element.zig");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const IS_DEBUG = builtin.mode == .Debug;
@@ -138,9 +136,9 @@ fn clearList(list: *std.DoublyLinkedList) void {
}
}
fn getHeaders(self: *ScriptManager, arena: Allocator, url: [:0]const u8) !net_http.Headers {
fn getHeaders(self: *ScriptManager) !net_http.Headers {
var headers = try self.client.newHeaders();
try self.page.headersForRequest(arena, url, &headers);
try self.page.headersForRequest(&headers);
return headers;
}
@@ -280,9 +278,10 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
.ctx = script,
.method = .GET,
.frame_id = page._frame_id,
.headers = try self.getHeaders(arena, url),
.headers = try self.getHeaders(),
.blocking = is_blocking,
.cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.resource_type = .script,
.notification = page._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
@@ -405,8 +404,9 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
.ctx = script,
.method = .GET,
.frame_id = page._frame_id,
.headers = try self.getHeaders(arena, url),
.headers = try self.getHeaders(),
.cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.resource_type = .script,
.notification = page._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
@@ -508,10 +508,11 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
.url = url,
.method = .GET,
.frame_id = page._frame_id,
.headers = try self.getHeaders(arena, url),
.headers = try self.getHeaders(),
.ctx = script,
.resource_type = .script,
.cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.notification = page._session.notification,
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
.header_callback = Script.headerCallback,
@@ -654,7 +655,6 @@ pub const Script = struct {
debug_transfer_aborted: bool = false,
debug_transfer_bytes_received: usize = 0,
debug_transfer_notified_fail: bool = false,
debug_transfer_redirecting: bool = false,
debug_transfer_intercept_state: u8 = 0,
debug_transfer_auth_challenge: bool = false,
debug_transfer_easy_id: usize = 0,
@@ -730,7 +730,6 @@ pub const Script = struct {
.a3 = self.debug_transfer_aborted,
.a4 = self.debug_transfer_bytes_received,
.a5 = self.debug_transfer_notified_fail,
.a6 = self.debug_transfer_redirecting,
.a7 = self.debug_transfer_intercept_state,
.a8 = self.debug_transfer_auth_challenge,
.a9 = self.debug_transfer_easy_id,
@@ -739,10 +738,9 @@ pub const Script = struct {
.b3 = transfer.aborted,
.b4 = transfer.bytes_received,
.b5 = transfer._notified_fail,
.b6 = transfer._redirecting,
.b7 = @intFromEnum(transfer._intercept_state),
.b8 = transfer._auth_challenge != null,
.b9 = if (transfer._conn) |c| @intFromPtr(c.easy) else 0,
.b9 = if (transfer._conn) |c| @intFromPtr(c._easy) else 0,
});
self.header_callback_called = true;
self.debug_transfer_id = transfer.id;
@@ -750,10 +748,9 @@ pub const Script = struct {
self.debug_transfer_aborted = transfer.aborted;
self.debug_transfer_bytes_received = transfer.bytes_received;
self.debug_transfer_notified_fail = transfer._notified_fail;
self.debug_transfer_redirecting = transfer._redirecting;
self.debug_transfer_intercept_state = @intFromEnum(transfer._intercept_state);
self.debug_transfer_auth_challenge = transfer._auth_challenge != null;
self.debug_transfer_easy_id = if (transfer._conn) |c| @intFromPtr(c.easy) else 0;
self.debug_transfer_easy_id = if (transfer._conn) |c| @intFromPtr(c._easy) else 0;
}
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });

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

@@ -357,25 +357,38 @@ pub fn isHTTPS(raw: [:0]const u8) bool {
pub fn getHostname(raw: [:0]const u8) []const u8 {
const host = getHost(raw);
const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return host;
return host[0..pos];
const port_sep = findPortSeparator(host) orelse return host;
return host[0..port_sep];
}
pub fn getPort(raw: [:0]const u8) []const u8 {
const host = getHost(raw);
const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return "";
const port_sep = findPortSeparator(host) orelse return "";
return host[port_sep + 1 ..];
}
if (pos + 1 >= host.len) {
return "";
// Finds the colon separating host from port, handling IPv6 bracket notation.
// For IPv6 like "[::1]:8080", returns position of ":" after "]".
// For IPv6 like "[::1]" (no port), returns null.
// For regular hosts, returns position of last ":" if followed by digits.
fn findPortSeparator(host: []const u8) ?usize {
if (host.len > 0 and host[0] == '[') {
// IPv6: find closing bracket, port separator must be after it
const bracket_end = std.mem.indexOfScalar(u8, host, ']') orelse return null;
if (bracket_end + 1 < host.len and host[bracket_end + 1] == ':') {
return bracket_end + 1;
}
return null;
}
// Regular host: find last colon and verify it's followed by digits
const pos = std.mem.lastIndexOfScalar(u8, host, ':') orelse return null;
if (pos + 1 >= host.len) return null;
for (host[pos + 1 ..]) |c| {
if (c < '0' or c > '9') {
return "";
}
if (c < '0' or c > '9') return null;
}
return host[pos + 1 ..];
return pos;
}
pub fn getSearch(raw: [:0]const u8) []const u8 {
@@ -403,21 +416,12 @@ pub fn getOrigin(allocator: Allocator, raw: [:0]const u8) !?[]const u8 {
return null;
}
var authority_start = scheme_end + 3;
const has_user_info = if (std.mem.indexOf(u8, raw[authority_start..], "@")) |pos| blk: {
authority_start += pos + 1;
break :blk true;
} else false;
// Find end of authority (start of path/query/fragment or end of string)
const authority_end_relative = std.mem.indexOfAny(u8, raw[authority_start..], "/?#");
const authority_end = if (authority_end_relative) |end|
authority_start + end
else
raw.len;
const auth = parseAuthority(raw) orelse return null;
const has_user_info = auth.has_user_info;
const authority_end = auth.host_end;
// Check for port in the host:port section
const host_part = raw[authority_start..authority_end];
const host_part = auth.getHost(raw);
if (std.mem.lastIndexOfScalar(u8, host_part, ':')) |colon_pos_in_host| {
const port = host_part[colon_pos_in_host + 1 ..];
@@ -458,31 +462,18 @@ pub fn getOrigin(allocator: Allocator, raw: [:0]const u8) !?[]const u8 {
}
fn getUserInfo(raw: [:0]const u8) ?[]const u8 {
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return null;
const auth = parseAuthority(raw) orelse return null;
if (!auth.has_user_info) return null;
// User info is from authority_start to host_start - 1 (excluding the @)
const scheme_end = std.mem.indexOf(u8, raw, "://").?;
const authority_start = scheme_end + 3;
const pos = std.mem.indexOfScalar(u8, raw[authority_start..], '@') orelse return null;
const path_start = std.mem.indexOfScalarPos(u8, raw, authority_start, '/') orelse raw.len;
const full_pos = authority_start + pos;
if (full_pos < path_start) {
return raw[authority_start..full_pos];
}
return null;
return raw[authority_start .. auth.host_start - 1];
}
pub fn getHost(raw: [:0]const u8) []const u8 {
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return "";
var authority_start = scheme_end + 3;
if (std.mem.indexOf(u8, raw[authority_start..], "@")) |pos| {
authority_start += pos + 1;
}
const authority = raw[authority_start..];
const path_start = std.mem.indexOfAny(u8, authority, "/?#") orelse return authority;
return authority[0..path_start];
const auth = parseAuthority(raw) orelse return "";
return auth.getHost(raw);
}
// Returns true if these two URLs point to the same document.
@@ -761,6 +752,47 @@ pub fn unescape(arena: Allocator, input: []const u8) ![]const u8 {
return result.items;
}
const AuthorityInfo = struct {
host_start: usize,
host_end: usize,
has_user_info: bool,
fn getHost(self: AuthorityInfo, raw: []const u8) []const u8 {
return raw[self.host_start..self.host_end];
}
};
// Parses the authority component of a URL, correctly handling userinfo.
// Returns null if the URL doesn't have a valid scheme (no "://").
// SECURITY: Only looks for @ within the authority portion (before /?#)
// to prevent path-based @ injection attacks.
fn parseAuthority(raw: []const u8) ?AuthorityInfo {
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return null;
const authority_start = scheme_end + 3;
// Find end of authority FIRST (start of path/query/fragment or end of string)
const authority_end = if (std.mem.indexOfAny(u8, raw[authority_start..], "/?#")) |end|
authority_start + end
else
raw.len;
// Only look for @ within the authority portion, not in path/query/fragment
const authority_portion = raw[authority_start..authority_end];
if (std.mem.indexOf(u8, authority_portion, "@")) |pos| {
return .{
.host_start = authority_start + pos + 1,
.host_end = authority_end,
.has_user_info = true,
};
}
return .{
.host_start = authority_start,
.host_end = authority_end,
.has_user_info = false,
};
}
const testing = @import("../testing.zig");
test "URL: isCompleteHTTPUrl" {
try testing.expectEqual(true, isCompleteHTTPUrl("http://example.com/about"));
@@ -1429,6 +1461,42 @@ test "URL: getHost" {
try testing.expectEqualSlices(u8, "example.com", getHost("https://user:pass@example.com/page"));
try testing.expectEqualSlices(u8, "example.com:8080", getHost("https://user:pass@example.com:8080/page"));
try testing.expectEqualSlices(u8, "", getHost("not-a-url"));
// SECURITY: @ in path must NOT be treated as userinfo separator
try testing.expectEqualSlices(u8, "evil.example.com", getHost("http://evil.example.com/@victim.example.com/"));
try testing.expectEqualSlices(u8, "evil.example.com", getHost("https://evil.example.com/path/@victim.example.com"));
// IPv6 addresses
try testing.expectEqualSlices(u8, "[::1]:8080", getHost("http://[::1]:8080/path"));
try testing.expectEqualSlices(u8, "[::1]", getHost("http://[::1]/path"));
try testing.expectEqualSlices(u8, "[2001:db8::1]", getHost("https://[2001:db8::1]/"));
}
test "URL: getHostname" {
// Regular hosts
try testing.expectEqualSlices(u8, "example.com", getHostname("https://example.com:8080/path"));
try testing.expectEqualSlices(u8, "example.com", getHostname("https://example.com/path"));
// IPv6 with port
try testing.expectEqualSlices(u8, "[::1]", getHostname("http://[::1]:8080/path"));
// IPv6 without port - must return full bracket notation
try testing.expectEqualSlices(u8, "[::1]", getHostname("http://[::1]/path"));
try testing.expectEqualSlices(u8, "[2001:db8::1]", getHostname("https://[2001:db8::1]/"));
}
test "URL: getPort" {
// Regular hosts
try testing.expectEqualSlices(u8, "8080", getPort("https://example.com:8080/path"));
try testing.expectEqualSlices(u8, "", getPort("https://example.com/path"));
// IPv6 with port
try testing.expectEqualSlices(u8, "8080", getPort("http://[::1]:8080/path"));
try testing.expectEqualSlices(u8, "3000", getPort("http://[2001:db8::1]:3000/"));
// IPv6 without port - colons inside brackets must not be treated as port separator
try testing.expectEqualSlices(u8, "", getPort("http://[::1]/path"));
try testing.expectEqualSlices(u8, "", getPort("https://[2001:db8::1]/"));
}
test "URL: setPathname percent-encodes" {
@@ -1449,3 +1517,56 @@ test "URL: setPathname percent-encodes" {
const result3 = try setPathname("https://example.com/path?a=b#hash", "/new path", allocator);
try testing.expectEqualSlices(u8, "https://example.com/new%20path?a=b#hash", result3);
}
test "URL: getOrigin" {
defer testing.reset();
const Case = struct {
url: [:0]const u8,
expected: ?[]const u8,
};
const cases = [_]Case{
// Basic HTTP/HTTPS origins
.{ .url = "http://example.com/path", .expected = "http://example.com" },
.{ .url = "https://example.com/path", .expected = "https://example.com" },
.{ .url = "https://example.com:8080/path", .expected = "https://example.com:8080" },
// Default ports should be stripped
.{ .url = "http://example.com:80/path", .expected = "http://example.com" },
.{ .url = "https://example.com:443/path", .expected = "https://example.com" },
// User info should be stripped from origin
.{ .url = "http://user:pass@example.com/path", .expected = "http://example.com" },
.{ .url = "https://user@example.com:8080/path", .expected = "https://example.com:8080" },
// Non-HTTP schemes return null
.{ .url = "ftp://example.com/path", .expected = null },
.{ .url = "file:///path/to/file", .expected = null },
.{ .url = "about:blank", .expected = null },
// Query and fragment should not affect origin
.{ .url = "https://example.com?query=1", .expected = "https://example.com" },
.{ .url = "https://example.com#fragment", .expected = "https://example.com" },
.{ .url = "https://example.com/path?q=1#frag", .expected = "https://example.com" },
// SECURITY: @ in path must NOT be treated as userinfo separator
// This would be a Same-Origin Policy bypass if mishandled
.{ .url = "http://evil.example.com/@victim.example.com/", .expected = "http://evil.example.com" },
.{ .url = "https://evil.example.com/path/@victim.example.com/steal", .expected = "https://evil.example.com" },
.{ .url = "http://evil.example.com/@victim.example.com:443/", .expected = "http://evil.example.com" },
// @ in query/fragment must also not affect origin
.{ .url = "https://example.com/path?user=foo@bar.com", .expected = "https://example.com" },
.{ .url = "https://example.com/path#user@host", .expected = "https://example.com" },
};
for (cases) |case| {
const result = try getOrigin(testing.arena_allocator, case.url);
if (case.expected) |expected| {
try testing.expectString(expected, result.?);
} else {
try testing.expectEqual(null, result);
}
}
}

View File

@@ -36,6 +36,7 @@ pub const InteractivityType = enum {
};
pub const InteractiveElement = struct {
backendNodeId: ?u32 = null,
node: *Node,
tag_name: []const u8,
role: ?[]const u8,
@@ -55,6 +56,11 @@ pub const InteractiveElement = struct {
pub fn jsonStringify(self: *const InteractiveElement, jw: anytype) !void {
try jw.beginObject();
if (self.backendNodeId) |id| {
try jw.objectField("backendNodeId");
try jw.write(id);
}
try jw.objectField("tagName");
try jw.write(self.tag_name);
@@ -123,6 +129,15 @@ pub const InteractiveElement = struct {
}
};
/// Populate backendNodeId on each interactive element by registering
/// their nodes in the given registry. Works with both CDP and MCP registries.
pub fn registerNodes(elements: []InteractiveElement, registry: anytype) !void {
for (elements) |*el| {
const registered = try registry.register(el.node);
el.backendNodeId = registered.id;
}
}
/// Collect all interactive elements under `root`.
pub fn collectInteractiveElements(
root: *Node,

View File

@@ -128,7 +128,7 @@ fn _constructor(self: *Caller, func: anytype, info: FunctionCallbackInfo) !void
const new_this_handle = info.getThis();
var this = js.Object{ .local = local, .handle = new_this_handle };
if (@typeInfo(ReturnType) == .error_union) {
const non_error_res = res catch |err| return err;
const non_error_res = try res;
this = try local.mapZigInstanceToJs(new_this_handle, non_error_res);
} else {
this = try local.mapZigInstanceToJs(new_this_handle, res);

View File

@@ -21,8 +21,8 @@ const lp = @import("lightpanda");
const log = @import("../../log.zig");
const js = @import("js.zig");
const Env = @import("Env.zig");
const bridge = @import("bridge.zig");
const Env = @import("Env.zig");
const Origin = @import("Origin.zig");
const Scheduler = @import("Scheduler.zig");
@@ -214,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 {
@@ -271,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

@@ -26,7 +26,6 @@ const App = @import("../../App.zig");
const log = @import("../../log.zig");
const bridge = @import("bridge.zig");
const Origin = @import("Origin.zig");
const Context = @import("Context.zig");
const Isolate = @import("Isolate.zig");
const Platform = @import("Platform.zig");
@@ -34,7 +33,6 @@ const Snapshot = @import("Snapshot.zig");
const Inspector = @import("Inspector.zig");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const Window = @import("../webapi/Window.zig");
const JsApis = bridge.JsApis;

View File

@@ -21,7 +21,6 @@ const js = @import("js.zig");
const v8 = js.v8;
const log = @import("../../log.zig");
const Session = @import("../Session.zig");
const Function = @This();
@@ -214,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

@@ -32,45 +32,15 @@ const js = @import("js.zig");
const Session = @import("../Session.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
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,11 +17,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Page = @import("../Page.zig");
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");
@@ -33,7 +33,6 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8;
const CallOpts = Caller.CallOpts;
const Allocator = std.mem.Allocator;
// Where js.Context has a lifetime tied to the page, and holds the
// v8::Global<v8::Context>, this has a much shorter lifetime and holds a
@@ -215,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);
@@ -264,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;
},
@@ -1123,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));
@@ -1155,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 {
@@ -1383,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

@@ -20,8 +20,6 @@ const std = @import("std");
const js = @import("js.zig");
const v8 = js.v8;
const Session = @import("../Session.zig");
const Promise = @This();
local: *const js.Local,
@@ -69,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

@@ -25,7 +25,6 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8;
const JsApis = bridge.JsApis;
const Allocator = std.mem.Allocator;
const Snapshot = @This();

View File

@@ -25,7 +25,6 @@ const v8 = js.v8;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Allocator = std.mem.Allocator;
const Session = @import("../Session.zig");
const Value = @This();
@@ -304,7 +303,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

@@ -18,15 +18,12 @@
const std = @import("std");
const js = @import("js.zig");
const lp = @import("lightpanda");
const log = @import("../../log.zig");
const Page = @import("../Page.zig");
const Session = @import("../Session.zig");
const v8 = js.v8;
const Caller = @import("Caller.zig");
const Context = @import("Context.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
@@ -104,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,
};
}
};
}
@@ -414,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;
@@ -853,6 +812,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/event/InputEvent.zig"),
@import("../webapi/event/PromiseRejectionEvent.zig"),
@import("../webapi/event/SubmitEvent.zig"),
@import("../webapi/event/FormDataEvent.zig"),
@import("../webapi/MessageChannel.zig"),
@import("../webapi/MessagePort.zig"),
@import("../webapi/media/MediaError.zig"),

54
src/browser/links.zig Normal file
View File

@@ -0,0 +1,54 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Element = @import("webapi/Element.zig");
const Node = @import("webapi/Node.zig");
const Page = @import("Page.zig");
const Selector = @import("webapi/selector/Selector.zig");
const Allocator = std.mem.Allocator;
/// Collect all links (href attributes from anchor tags) under `root`.
/// Returns a slice of strings allocated with `arena`.
pub fn collectLinks(arena: Allocator, root: *Node, page: *Page) ![]const []const u8 {
var links: std.ArrayList([]const u8) = .empty;
if (Selector.querySelectorAll(root, "a[href]", page)) |list| {
defer list.deinit(page._session);
for (list._nodes) |node| {
if (node.is(Element.Html.Anchor)) |anchor| {
const href = anchor.getHref(page) catch |err| {
@import("../lightpanda.zig").log.err(.app, "resolve href failed", .{ .err = err });
continue;
};
if (href.len > 0) {
try links.append(arena, href);
}
}
}
} else |err| {
@import("../lightpanda.zig").log.err(.app, "query links failed", .{ .err = err });
return err;
}
return links.items;
}

View File

@@ -21,7 +21,6 @@ const std = @import("std");
const Page = @import("Page.zig");
const URL = @import("URL.zig");
const TreeWalker = @import("webapi/TreeWalker.zig");
const CData = @import("webapi/CData.zig");
const Element = @import("webapi/Element.zig");
const Node = @import("webapi/Node.zig");
const isAllWhitespace = @import("../string.zig").isAllWhitespace;

View File

@@ -125,6 +125,19 @@
</script>
<script id="CanvasRenderingContext2D#canvas">
{
const element = document.createElement("canvas");
const ctx = element.getContext("2d");
testing.expectEqual(ctx.canvas, element);
// Setting dimensions via ctx.canvas should update the element.
ctx.canvas.width = 40;
ctx.canvas.height = 25;
testing.expectEqual(element.width, 40);
testing.expectEqual(element.height, 25);
}
</script>
<script id="getter">
{
const element = document.createElement("canvas");
@@ -135,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

@@ -734,3 +734,101 @@
testing.expectEqual([['field', 'data'], ['x', '0'], ['y', '0']], entries);
}
</script>
<script id=formDataEventFires>
{
// formdata event fires on the form when FormData is constructed with a form
const form = document.createElement('form');
const input = document.createElement('input');
input.name = 'field';
input.value = 'hello';
form.appendChild(input);
let eventFired = false;
let receivedFormData = null;
form.addEventListener('formdata', (e) => {
eventFired = true;
receivedFormData = e.formData;
});
const fd = new FormData(form);
testing.expectEqual(true, eventFired);
testing.expectEqual(fd, receivedFormData);
}
</script>
<script id=formDataEventNotFiredWithoutForm>
{
// formdata event should NOT fire when FormData is constructed without a form
const fd = new FormData();
fd.append('a', '1');
testing.expectEqual('1', fd.get('a'));
}
</script>
<script id=formDataEventBubbles>
{
// formdata event should bubble
const container = document.createElement('div');
const form = document.createElement('form');
container.appendChild(form);
document.body.appendChild(container);
const input = document.createElement('input');
input.name = 'x';
input.value = '1';
form.appendChild(input);
let bubbled = false;
container.addEventListener('formdata', () => {
bubbled = true;
});
const fd = new FormData(form);
testing.expectEqual(true, bubbled);
document.body.removeChild(container);
}
</script>
<script id=formDataEventNotCancelable>
{
// formdata event should not be cancelable
const form = document.createElement('form');
const input = document.createElement('input');
input.name = 'key';
input.value = 'val';
form.appendChild(input);
let cancelable = null;
form.addEventListener('formdata', (e) => {
cancelable = e.cancelable;
});
const fd = new FormData(form);
testing.expectEqual(false, cancelable);
}
</script>
<script id=formDataEventModifyFormData>
{
// Listeners can modify formData during the event
const form = document.createElement('form');
const input = document.createElement('input');
input.name = 'original';
input.value = 'data';
form.appendChild(input);
form.addEventListener('formdata', (e) => {
e.formData.append('added', 'by-listener');
});
const fd = new FormData(form);
testing.expectEqual('data', fd.get('original'));
testing.expectEqual('by-listener', fd.get('added'));
}
</script>

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

@@ -24,7 +24,6 @@ const Page = @import("../Page.zig");
const Node = @import("Node.zig");
const Element = @import("Element.zig");
const DOMException = @import("DOMException.zig");
const Custom = @import("element/html/Custom.zig");
const CustomElementDefinition = @import("CustomElementDefinition.zig");

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

@@ -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,
@@ -77,6 +79,7 @@ pub const Type = union(enum) {
ui_event: *@import("event/UIEvent.zig"),
promise_rejection_event: *@import("event/PromiseRejectionEvent.zig"),
submit_event: *@import("event/SubmitEvent.zig"),
form_data_event: *@import("event/FormDataEvent.zig"),
};
pub const Options = struct {
@@ -138,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 {
@@ -176,6 +170,7 @@ pub fn is(self: *Event, comptime T: type) ?*T {
.pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null,
.promise_rejection_event => |e| return if (T == @import("event/PromiseRejectionEvent.zig")) e else null,
.submit_event => |e| return if (T == @import("event/SubmitEvent.zig")) e else null,
.form_data_event => |e| return if (T == @import("event/FormDataEvent.zig")) e else null,
.ui_event => |e| {
if (T == @import("event/UIEvent.zig")) {
return e;
@@ -438,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

@@ -18,7 +18,6 @@
const std = @import("std");
const js = @import("../js/js.zig");
const String = @import("../../string.zig").String;
const Page = @import("../Page.zig");
const Node = @import("Node.zig");
@@ -183,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));
}
@@ -251,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

@@ -19,10 +19,8 @@
const std = @import("std");
const String = @import("../../string.zig").String;
const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const color = @import("../color.zig");
const Page = @import("../Page.zig");
/// https://developer.mozilla.org/en-US/docs/Web/API/ImageData/ImageData

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

@@ -28,8 +28,6 @@ const DocumentFragment = @import("DocumentFragment.zig");
const AbstractRange = @import("AbstractRange.zig");
const DOMRect = @import("DOMRect.zig");
const Allocator = std.mem.Allocator;
const Range = @This();
_proto: *AbstractRange,
@@ -40,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;
}
@@ -699,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

@@ -62,5 +62,6 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(ResizeObserver.init, .{});
pub const observe = bridge.function(ResizeObserver.observe, .{});
pub const unobserve = bridge.function(ResizeObserver.unobserve, .{});
pub const disconnect = bridge.function(ResizeObserver.disconnect, .{});
};

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../log.zig");
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
@@ -27,25 +27,33 @@ const Range = @import("Range.zig");
const AbstractRange = @import("AbstractRange.zig");
const Node = @import("Node.zig");
const Event = @import("Event.zig");
const Document = @import("Document.zig");
/// https://w3c.github.io/selection-api/
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);
@@ -695,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();
@@ -710,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

@@ -19,7 +19,6 @@
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const EventTarget = @import("EventTarget.zig");
const Window = @import("Window.zig");
const VisualViewport = @This();

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

@@ -23,16 +23,24 @@ const js = @import("../../js/js.zig");
const color = @import("../../color.zig");
const Page = @import("../../Page.zig");
const Canvas = @import("../element/html/Canvas.zig");
const ImageData = @import("../ImageData.zig");
/// This class doesn't implement a `constructor`.
/// It can be obtained with a call to `HTMLCanvasElement#getContext`.
/// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
const CanvasRenderingContext2D = @This();
/// Reference to the parent canvas element.
/// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/canvas
_canvas: *Canvas,
/// Fill color.
/// TODO: Add support for `CanvasGradient` and `CanvasPattern`.
_fill_style: color.RGBA = color.RGBA.Named.black,
pub fn getCanvas(self: *const CanvasRenderingContext2D) *Canvas {
return self._canvas;
}
pub fn getFillStyle(self: *const CanvasRenderingContext2D, page: *Page) ![]const u8 {
var w = std.Io.Writer.Allocating.init(page.call_arena);
try self._fill_style.format(&w.writer);
@@ -125,6 +133,7 @@ pub const JsApi = struct {
pub var class_id: bridge.ClassId = undefined;
};
pub const canvas = bridge.accessor(CanvasRenderingContext2D.getCanvas, null, .{});
pub const font = bridge.property("10px sans-serif", .{ .template = false, .readonly = false });
pub const globalAlpha = bridge.property(1.0, .{ .template = false, .readonly = false });
pub const globalCompositeOperation = bridge.property("source-over", .{ .template = false, .readonly = false });

View File

@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const CData = @import("../CData.zig");

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

@@ -20,7 +20,6 @@
const std = @import("std");
const lp = @import("lightpanda");
const log = @import("../../../log.zig");
const crypto = @import("../../../sys/libcrypto.zig");
const Page = @import("../../Page.zig");

View File

@@ -20,12 +20,10 @@
const std = @import("std");
const lp = @import("lightpanda");
const log = @import("../../../log.zig");
const crypto = @import("../../../sys/libcrypto.zig");
const Page = @import("../../Page.zig");
const js = @import("../../js/js.zig");
const Algorithm = @import("algorithm.zig").Algorithm;
const CryptoKey = @import("../CryptoKey.zig");

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

@@ -22,7 +22,6 @@ const reflect = @import("../../reflect.zig");
const log = @import("../../../log.zig");
const global_event_handlers = @import("../global_event_handlers.zig");
const GlobalEventHandlersLookup = global_event_handlers.Lookup;
const GlobalEventHandler = global_event_handlers.Handler;
const Page = @import("../../Page.zig");
@@ -376,6 +375,22 @@ 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 getAttributeFunction(
self: *HtmlElement,
listener_type: GlobalEventHandler,
@@ -1212,7 +1227,9 @@ 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 onabort = bridge.accessor(HtmlElement.getOnAbort, HtmlElement.setOnAbort, .{});

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;
@@ -67,18 +70,29 @@ const DrawingContext = union(enum) {
webgl: *WebGLRenderingContext,
};
pub fn getContext(_: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext {
if (std.mem.eql(u8, context_type, "2d")) {
const ctx = try page._factory.create(CanvasRenderingContext2D{});
return .{ .@"2d" = ctx };
pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext {
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

@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const log = @import("../../../../log.zig");
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");
const Window = @import("../../Window.zig");

View File

@@ -5,10 +5,6 @@ const URL = @import("../../../URL.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const Event = @import("../../Event.zig");
const log = @import("../../../../log.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const Image = @This();
_proto: *HtmlElement,

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const log = @import("../../../../log.zig");
const js = @import("../../../js/js.zig");
const Page = @import("../../../Page.zig");

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

@@ -22,7 +22,6 @@ const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const CompositionEvent = @This();
@@ -54,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;
}
@@ -73,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

@@ -0,0 +1,88 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const String = @import("../../../string.zig").String;
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const js = @import("../../js/js.zig");
const Event = @import("../Event.zig");
const FormData = @import("../net/FormData.zig");
/// https://developer.mozilla.org/en-US/docs/Web/API/FormDataEvent
const FormDataEvent = @This();
_proto: *Event,
_form_data: ?*FormData = null,
const Options = Event.inheritOptions(FormDataEvent, struct {
formData: ?*FormData = null,
});
pub fn init(typ: []const u8, maybe_options: Options, page: *Page) !*FormDataEvent {
const arena = try page.getArena(.{ .debug = "FormDataEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, maybe_options, false, page);
}
pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*FormDataEvent {
const arena = try page.getArena(.{ .debug = "FormDataEvent.trusted" });
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, _opts, true, page);
}
fn initWithTrusted(arena: Allocator, typ: String, maybe_options: ?Options, trusted: bool, page: *Page) !*FormDataEvent {
const options = maybe_options orelse Options{};
const event = try page._factory.event(
arena,
typ,
FormDataEvent{
._proto = undefined,
._form_data = options.formData,
},
);
Event.populatePrototypes(event, options, trusted);
return event;
}
pub fn asEvent(self: *FormDataEvent) *Event {
return self._proto;
}
pub fn getFormData(self: *const FormDataEvent) ?*FormData {
return self._form_data;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(FormDataEvent);
pub const Meta = struct {
pub const name = "FormDataEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(FormDataEvent.init, .{});
pub const formData = bridge.accessor(FormDataEvent.getFormData, null, .{});
};

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

@@ -22,7 +22,6 @@ const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const PromiseRejectionEvent = @This();
@@ -57,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 {
@@ -86,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

@@ -26,7 +26,7 @@ const Event = @import("../Event.zig");
const HtmlElement = @import("../element/Html.zig");
const Allocator = std.mem.Allocator;
// https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent
/// https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent
const SubmitEvent = @This();
_proto: *Event,
@@ -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

@@ -27,8 +27,6 @@ const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const EventTarget = @import("../EventTarget.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
// https://developer.mozilla.org/en-US/docs/Web/API/Navigation
const Navigation = @This();

View File

@@ -18,7 +18,6 @@
const std = @import("std");
const URL = @import("../URL.zig");
const EventTarget = @import("../EventTarget.zig");
const NavigationState = @import("root.zig").NavigationState;
const Page = @import("../../Page.zig");
const js = @import("../../js/js.zig");

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.* = .{
@@ -80,7 +80,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
if (request._headers) |h| {
try h.populateHttpHeader(page.call_arena, &headers);
}
try page.headersForRequest(page.arena, request._url, &headers);
try page.headersForRequest(&headers);
if (comptime IS_DEBUG) {
log.debug(.http, "fetch", .{ .url = request._url });
@@ -95,6 +95,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
.headers = headers,
.resource_type = .fetch,
.cookie_jar = &page._session.cookie_jar,
.cookie_origin = page.url,
.notification = page._session.notification,
.start_callback = httpStartCallback,
.header_callback = httpHeaderDoneCallback,
@@ -225,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;
@@ -234,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;
};
@@ -256,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

@@ -22,7 +22,6 @@ const log = @import("../../../log.zig");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Node = @import("../Node.zig");
const Form = @import("../element/html/Form.zig");
const Element = @import("../Element.zig");
const KeyValueList = @import("../KeyValueList.zig");
@@ -35,10 +34,22 @@ _arena: Allocator,
_list: KeyValueList,
pub fn init(form: ?*Form, submitter: ?*Element, page: *Page) !*FormData {
return page._factory.create(FormData{
const form_data = try page._factory.create(FormData{
._arena = page.arena,
._list = try collectForm(page.arena, form, submitter, page),
});
// Dispatch `formdata` event if form provided.
if (form) |_form| {
const form_data_event = try (@import("../event/FormDataEvent.zig")).initTrusted(
comptime .wrap("formdata"),
.{ .bubbles = true, .cancelable = false, .formData = form_data },
page,
);
try page._event_manager.dispatch(_form.asNode().asEventTarget(), form_data_event.asEvent());
}
return form_data;
}
pub fn get(self: *const FormData, name: []const u8) ?[]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 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

@@ -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 log = @import("../../../log.zig");
@@ -29,7 +30,6 @@ const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const Node = @import("../Node.zig");
const Blob = @import("../Blob.zig");
const Event = @import("../Event.zig");
const Headers = @import("Headers.zig");
const EventTarget = @import("../EventTarget.zig");
@@ -39,6 +39,7 @@ 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,
@@ -88,21 +89,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;
}
@@ -138,6 +136,14 @@ pub fn deinit(self: *XMLHttpRequest, shutdown: bool, session: *Session) void {
session.releaseArena(self._arena);
}
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;
}
@@ -225,7 +231,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
try self._request_headers.populateHttpHeader(page.call_arena, &headers);
if (cookie_support) {
try page.headersForRequest(self._arena, self._url, &headers);
try page.headersForRequest(&headers);
}
try http_client.request(.{
@@ -236,6 +242,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
.frame_id = page._frame_id,
.body = self._request_body,
.cookie_jar = if (cookie_support) &page._session.cookie_jar else null,
.cookie_origin = page.url,
.resource_type = .xhr,
.notification = page._session.notification,
.start_callback = httpStartCallback,
@@ -245,8 +252,6 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
.error_callback = httpErrorCallback,
.shutdown_callback = httpShutdownCallback,
});
page.js.strongRef(self);
}
fn handleBlobUrl(self: *XMLHttpRequest, page: *Page) !void {
@@ -388,6 +393,7 @@ fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
log.debug(.http, "request start", .{ .method = self._method, .url = self._url, .source = "xhr" });
}
self._transfer = transfer;
self.acquireRef();
}
fn httpHeaderCallback(transfer: *HttpClient.Transfer, header: net_http.Header) !void {
@@ -486,15 +492,17 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
.loaded = loaded,
}, page);
page.js.weakRef(self);
self.releaseRef(page._session);
}
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.releaseRef(self._page._session);
}
}
fn httpShutdownCallback(ctx: *anyopaque) void {
@@ -505,10 +513,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.releaseRef(self._page._session);
}
self._page.js.weakRef(self);
}
fn handleError(self: *XMLHttpRequest, err: anyerror) void {
@@ -582,8 +590,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, .{});

File diff suppressed because it is too large Load Diff

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

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