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.
- package/ai-debug.log +5537 -0
- package/lib/audio/playback-buffer.js +27 -0
- package/lib/audio/pulse-client.js +18 -8
- package/lib/cdp-server.js +6 -0
- package/lib/happy-dom/parser.js +5 -0
- package/lib/happy-dom/selector-pseudo-adapter.js +14 -0
- package/lib/headless-shell.js +3 -0
- package/lib/layout/text-layout.js +76 -40
- package/lib/media/video-keyframe.js +29 -0
- package/lib/paint/display-list.js +20 -5
- package/lib/render-tree/index.js +2 -0
- package/lib/renderer/bindings.js +11 -2
- package/lib/renderer/compositor.js +16 -1
- package/lib/renderer/custom-element-reactions.js +18 -0
- package/lib/renderer/history.js +42 -0
- package/lib/renderer/host.js +8 -1
- package/lib/renderer/image-loading.js +27 -0
- package/lib/renderer/keyboard-input.js +12 -0
- package/lib/renderer/main.js +3 -1
- package/lib/renderer/media-playback-events.js +13 -0
- package/lib/renderer/page-realm.js +9 -50
- package/lib/renderer/page-renderer.js +206 -79
- package/lib/renderer/piano-input.js +22 -0
- package/lib/renderer/pointer-move.js +29 -0
- package/lib/renderer/video-presentation.js +126 -0
- package/lib/style/computed-style.js +24 -7
- package/package.json +1 -1
- package/test/compositor.test.js +38 -0
- package/test/computed-style.test.js +21 -0
- package/test/display-list.test.js +14 -0
- package/test/download.test.js +8 -0
- package/test/headless-shell.test.js +22 -0
- package/test/media-playback-events.test.js +22 -0
- package/test/page-scripts.test.js +159 -1
- package/test/piano-input.test.js +44 -0
- package/test/playback-buffer.test.js +31 -0
- package/test/pulse-client.test.js +17 -2
- package/test/text-layout.test.js +62 -0
- 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
|
+
}
|
package/lib/renderer/host.js
CHANGED
|
@@ -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)
|
|
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
|
+
}
|
package/lib/renderer/main.js
CHANGED
|
@@ -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) => {
|