buninu-browser 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/ai-debug.log +5537 -0
  2. package/lib/audio/playback-buffer.js +27 -0
  3. package/lib/audio/pulse-client.js +18 -8
  4. package/lib/cdp-server.js +6 -0
  5. package/lib/happy-dom/parser.js +5 -0
  6. package/lib/happy-dom/selector-pseudo-adapter.js +14 -0
  7. package/lib/headless-shell.js +3 -0
  8. package/lib/layout/text-layout.js +76 -40
  9. package/lib/media/video-keyframe.js +29 -0
  10. package/lib/paint/display-list.js +20 -5
  11. package/lib/render-tree/index.js +2 -0
  12. package/lib/renderer/bindings.js +11 -2
  13. package/lib/renderer/compositor.js +16 -1
  14. package/lib/renderer/custom-element-reactions.js +18 -0
  15. package/lib/renderer/history.js +42 -0
  16. package/lib/renderer/host.js +8 -1
  17. package/lib/renderer/image-loading.js +27 -0
  18. package/lib/renderer/keyboard-input.js +12 -0
  19. package/lib/renderer/main.js +3 -1
  20. package/lib/renderer/media-playback-events.js +13 -0
  21. package/lib/renderer/page-realm.js +9 -50
  22. package/lib/renderer/page-renderer.js +206 -79
  23. package/lib/renderer/piano-input.js +22 -0
  24. package/lib/renderer/pointer-move.js +29 -0
  25. package/lib/renderer/video-presentation.js +126 -0
  26. package/lib/style/computed-style.js +24 -7
  27. package/package.json +1 -1
  28. package/test/compositor.test.js +38 -0
  29. package/test/computed-style.test.js +21 -0
  30. package/test/display-list.test.js +14 -0
  31. package/test/download.test.js +8 -0
  32. package/test/headless-shell.test.js +22 -0
  33. package/test/media-playback-events.test.js +22 -0
  34. package/test/page-scripts.test.js +159 -1
  35. package/test/piano-input.test.js +44 -0
  36. package/test/playback-buffer.test.js +31 -0
  37. package/test/pulse-client.test.js +17 -2
  38. package/test/text-layout.test.js +62 -0
  39. package/test/video-presentation.test.js +67 -0
@@ -0,0 +1,42 @@
1
+ // Methods borrowed from another window's History.prototype must operate on
2
+ // their receiver, not the window in which the function was created.
3
+ const histories = new WeakMap();
4
+
5
+ export function historyBinding(location, setHref) {
6
+ const state = { location, setHref, value: null, length: 1 };
7
+ const receiver = (object) => {
8
+ const record = histories.get(object);
9
+ if (!record) throw new TypeError("Illegal invocation");
10
+ return record;
11
+ };
12
+ const change = (object, data, url, push) => {
13
+ const record = receiver(object);
14
+ if (url !== undefined && url !== null) {
15
+ const target = new URL(String(url), record.location.href);
16
+ if (target.origin !== new URL(record.location.href).origin) {
17
+ throw new DOMException("History state URL must have the document's origin.", "SecurityError");
18
+ }
19
+ record.setHref(target.href);
20
+ }
21
+ record.value = data ?? null;
22
+ if (push) record.length++;
23
+ };
24
+ function History() { throw new TypeError("Illegal constructor"); }
25
+ History.prototype = {
26
+ constructor: History,
27
+ get length() { return receiver(this).length; },
28
+ get state() { return receiver(this).value; },
29
+ scrollRestoration: "auto",
30
+ back() {},
31
+ forward() {},
32
+ go() {},
33
+ pushState(data, _title, url = undefined) { change(this, data, url, true); },
34
+ replaceState(data, _title, url = undefined) { change(this, data, url, false); },
35
+ };
36
+ const history = Object.create(History.prototype);
37
+ histories.set(history, state);
38
+ return { History, history, pushEntry(replace) {
39
+ state.value = null;
40
+ if (!replace) state.length++;
41
+ } };
42
+ }
@@ -489,7 +489,14 @@ export class RendererHost {
489
489
  /** A renderer's audio element starts playing: a PulseAudio stream relays its PCM. */
490
490
  #openAudio(child, { id, rate, channels, name }) {
491
491
  const send = (payload) => {
492
- if (this.#child === child) child.send({ type: "audio-event", id, ...payload });
492
+ if (this.#child === child) {
493
+ child.send({ type: "audio-event", id, ...payload });
494
+ // The compositor's A/V clock must not wait behind page JavaScript.
495
+ if (payload.event === "timing" && this.#channel && !this.#channel.closed) {
496
+ this.#channel.writer.write(`${JSON.stringify({ type: "audio-timing", audioId: id, seconds: payload.seconds })}\n`);
497
+ this.#channel.writer.flush();
498
+ }
499
+ }
493
500
  };
494
501
  const owned = [...this.#audio.values()].filter((entry) => entry.child === child).length;
495
502
  if (this.#audio.has(id) || owned >= MAX_AUDIO_STREAMS
@@ -0,0 +1,27 @@
1
+ // Image bytes are decoded by our renderer, not Happy DOM's disabled loader.
2
+ // Share their state with HTMLImageElement's page-facing IDL attributes.
3
+ const states = new WeakMap();
4
+
5
+ export function beginImageLoad(element, src) {
6
+ states.set(element, { src, complete: false, width: 0, height: 0 });
7
+ }
8
+
9
+ export function finishImageLoad(element, src, resource, dispatch = () => {}) {
10
+ if (element.getAttribute('src') !== src) return;
11
+ states.set(element, { src, complete: true, width: resource?.width ?? 0, height: resource?.height ?? 0 });
12
+ dispatch(resource ? 'load' : 'error');
13
+ }
14
+
15
+ export function installImageLoadingAttributes(prototype, nodeOf) {
16
+ for (const [property, field] of [['naturalWidth', 'width'], ['naturalHeight', 'height'], ['complete', 'complete']]) {
17
+ Object.defineProperty(prototype, property, {
18
+ configurable: true, enumerable: true,
19
+ get() {
20
+ const node = nodeOf(this), src = node.getAttribute('src');
21
+ const state = states.get(node);
22
+ if (state && state.src === src) return state[field];
23
+ return field === 'complete' ? !src : 0;
24
+ },
25
+ });
26
+ }
27
+ }
@@ -0,0 +1,12 @@
1
+ // Preserve CDP's virtual key codes; infer them for the direct press API too.
2
+ const codes = { Backspace:8, Tab:9, Enter:13, Shift:16, Control:17, Alt:18,
3
+ Escape:27, ' ':32, Space:32, PageUp:33, PageDown:34, End:35, Home:36,
4
+ ArrowLeft:37, ArrowUp:38, ArrowRight:39, ArrowDown:40, Insert:45, Delete:46 };
5
+ export function keyboardInput(key, options = {}) {
6
+ key = String(key);
7
+ const code = String(options.code ?? '');
8
+ const inferred = codes[key] ?? codes[code]
9
+ ?? (/^[a-z0-9]$/i.test(key) ? key.toUpperCase().charCodeAt(0) : 0);
10
+ const keyCode = Number(options.windowsVirtualKeyCode ?? options.keyCode ?? inferred) || 0;
11
+ return { key, code, keyCode, which:keyCode, charCode:0, composed:true };
12
+ }
@@ -52,7 +52,7 @@ const DANGEROUSLY_ALLOW_HOST_JS = "--dangerously-allow-host-js";
52
52
  /** Methods the controller may invoke; anything else is ignored. */
53
53
  const METHODS = new Set([
54
54
  "loadDocument", "showFragment", "wheel", "runJavaScriptURL", "url", "scrollTo", "resize", "screenshot", "title",
55
- "scroll", "press", "type", "scrollOffset", "click", "evaluate", "setUserAgent", "setInitScripts", "consoleMessages", "whenLoaded",
55
+ "scroll", "press", "type", "scrollOffset", "click", "move", "evaluate", "setUserAgent", "setInitScripts", "consoleMessages", "whenLoaded",
56
56
  "applyCompositorScroll",
57
57
  ]);
58
58
 
@@ -169,6 +169,7 @@ async function startCompositor() {
169
169
  });
170
170
  worker.addEventListener("message", ({ data }) => {
171
171
  if (data?.type === "scroll") renderer.applyCompositorScroll(data.x, data.y);
172
+ else if (data?.type === "video-report") renderer.videoReport(data);
172
173
  else if (data?.type === "frame-ack" && Number.isInteger(data.serial)) {
173
174
  framesAcknowledged = Math.max(framesAcknowledged, data.serial);
174
175
  frameWaiters = frameWaiters.filter((waiter) => {
@@ -232,6 +233,7 @@ const renderer = await createPageRenderer({
232
233
  consoleMessage: logging ? (level, text) => report(`page console.${level}: ${text}`) : null,
233
234
  trace: logging ? (text) => report(`timing: ${text}`) : null,
234
235
  onFrame: compositor ? (frame) => compositor.postMessage({ type: "frame", serial: ++framesSent, ...frame }) : null,
236
+ onVideo: compositor ? (message) => compositor.postMessage(message) : null,
235
237
  // Whoever waits for load (a screenshot, say) sees the frame painted at load.
236
238
  onLifecycle: (name, generation) => {
237
239
  if (name !== "load") process.send({ type: "lifecycle", name, generation });
@@ -0,0 +1,13 @@
1
+ // `play` means unpaused; `playing` means playback can actually advance.
2
+ // Keep the distinction across pauses and network starvation.
3
+ const phases = new WeakMap();
4
+ export function playbackEvent(state, phase, dispatch) {
5
+ const previous = phases.get(state);
6
+ if (previous === phase) return;
7
+ phases.set(state, phase);
8
+ if (phase === 'playing' && previous === 'waiting') {
9
+ dispatch(state, 'canplay');
10
+ if (phases.get(state) !== phase || !state.playing) return;
11
+ }
12
+ dispatch(state, phase);
13
+ }
@@ -29,6 +29,9 @@ import { markNative, markNativeApi, setFunctionSourceTextTransform } from "./nat
29
29
  import { applyGlobalShape, platformObject, storageInterface } from "./interfaces.js";
30
30
  import { trustedTypesApi } from "./trusted-types.js";
31
31
  import { onPromiseRejection } from "./rejections.js";
32
+ import { installAttributeReactions } from "./custom-element-reactions.js";
33
+ import { historyBinding } from "./history.js";
34
+ import { installImageLoadingAttributes } from "./image-loading.js";
32
35
  import { AsyncLocalStorage } from "node:async_hooks";
33
36
 
34
37
  // URL is one shared intrinsic, hardened before any page sees it. Blob URL
@@ -1476,6 +1479,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1476
1479
  return Promise.resolve(values);
1477
1480
  },
1478
1481
  };
1482
+ installImageLoadingAttributes(elementInterfacePrototypes.get("HTMLImageElement"), nodeOf);
1479
1483
  const { location, setHref } = locationBinding(document.URL, hooks.navigate);
1480
1484
  const { History, history, pushEntry } = historyBinding(location, setHref);
1481
1485
  const { Event, CustomEvent, ...uiEvents } = eventConstructors(realm);
@@ -1610,6 +1614,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1610
1614
  if (typeof wrapper.disconnectedCallback === "function") callPage(wrapper.disconnectedCallback, wrapper, []);
1611
1615
  }
1612
1616
  };
1617
+ installAttributeReactions(Adapter, constructor, realm, callPage);
1613
1618
  window.customElements.define(tag, Adapter);
1614
1619
  customElementConstructors.set(tag, constructor);
1615
1620
  // Upgrades change :defined without a DOM mutation.
@@ -2478,56 +2483,6 @@ function directEvalHelper(compartment, pageGlobal, scriptText) {
2478
2483
  };
2479
2484
  }
2480
2485
 
2481
- /**
2482
- * window.History and window.history for one document. The methods live on a
2483
- * per-document History.prototype that pages may wrap (dev servers do, to see
2484
- * client-side routing). pushState/replaceState change the URL and state
2485
- * without navigating or firing events; traversal is not modelled.
2486
- */
2487
- function historyBinding(location, setHref) {
2488
- let state = null;
2489
- let length = 1;
2490
- const change = (data, url) => {
2491
- if (url === undefined || url === null) return;
2492
- const target = new URL(String(url), location.href);
2493
- if (target.origin !== new URL(location.href).origin) {
2494
- throw new DOMException(`A history state object with URL '${target.href}' cannot be created in a document with origin '${new URL(location.href).origin}'.`, "SecurityError");
2495
- }
2496
- setHref(target.href);
2497
- };
2498
- function History() {
2499
- throw new TypeError("Illegal constructor");
2500
- }
2501
- History.prototype = {
2502
- constructor: History,
2503
- get length() {
2504
- return length;
2505
- },
2506
- get state() {
2507
- return state;
2508
- },
2509
- scrollRestoration: "auto",
2510
- back() {},
2511
- forward() {},
2512
- go() {},
2513
- pushState(data, _title, url = undefined) {
2514
- change(data, url);
2515
- state = data ?? null;
2516
- length++;
2517
- },
2518
- replaceState(data, _title, url = undefined) {
2519
- change(data, url);
2520
- state = data ?? null;
2521
- },
2522
- };
2523
- // A fragment navigation adds an entry with no state (a same-URL one replaces it).
2524
- const pushEntry = (replace) => {
2525
- state = null;
2526
- if (!replace) length++;
2527
- };
2528
- return { History, history: Object.create(History.prototype), pushEntry };
2529
- }
2530
-
2531
2486
  /** window.location; `setHref` moves it for same-document (fragment) navigations. */
2532
2487
  function locationBinding(href, navigate) {
2533
2488
  const url = () => new URL(href);
@@ -2787,6 +2742,8 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2787
2742
  lineHeight: style.lineHeight == null ? "normal" : px(style.lineHeight),
2788
2743
  textAlign: style.textAlign,
2789
2744
  whiteSpace: style.whiteSpace,
2745
+ WebkitLineClamp: style.webkitLineClamp == null ? "none" : String(style.webkitLineClamp),
2746
+ WebkitBoxOrient: style.webkitBoxOrient,
2790
2747
  verticalAlign: typeof style.verticalAlign === "number" ? px(style.verticalAlign) : String(style.verticalAlign),
2791
2748
  textOverflow: style.textOverflow,
2792
2749
  boxSizing: style.boxSizing,
@@ -2843,6 +2800,8 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
2843
2800
  };
2844
2801
  return {
2845
2802
  ...values,
2803
+ webkitLineClamp: values.WebkitLineClamp,
2804
+ webkitBoxOrient: values.WebkitBoxOrient,
2846
2805
  length: names.length,
2847
2806
  item: (index) => names[Number(index)] ?? "",
2848
2807
  getPropertyValue: (name) => {