buninu-browser 0.5.1 → 0.6.0
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/README.md +9 -3
- package/lib/audio/pcm.js +11 -0
- package/lib/audio/pulse-client.js +7 -2
- package/lib/cdp-server.js +31 -7
- package/lib/happy-dom/document-base.js +29 -1
- package/lib/happy-dom/mutation-observer.js +157 -0
- package/lib/happy-dom/parser.js +2 -0
- package/lib/headless-shell.js +49 -9
- package/lib/layout/repaint.js +89 -0
- package/lib/media/mp4-keyframes.js +144 -0
- package/lib/media/video-keyframe.js +29 -1
- package/lib/media/webm-snapshots.js +282 -0
- package/lib/network/cookie-jar.js +18 -2
- package/lib/paint/display-list.js +4 -3
- package/lib/paint/frame.js +12 -5
- package/lib/render-tree/index.js +3 -0
- package/lib/renderer/animation-frames.js +76 -0
- package/lib/renderer/bindings.js +2 -0
- package/lib/renderer/compositor-channel.js +95 -10
- package/lib/renderer/compositor.js +54 -18
- package/lib/renderer/document-cookies.js +87 -0
- package/lib/renderer/font-loading.js +60 -16
- package/lib/renderer/host.js +48 -19
- package/lib/renderer/idle-callbacks.js +109 -0
- package/lib/renderer/main.js +5 -1
- package/lib/renderer/media-source.js +4 -0
- package/lib/renderer/page-realm.js +159 -49
- package/lib/renderer/page-renderer.js +567 -197
- package/lib/renderer/resource-elements.js +88 -0
- package/lib/renderer/script-rewrite.js +18 -1
- package/lib/renderer/seccomp.js +6 -4
- package/lib/renderer/timers.js +71 -0
- package/lib/renderer/video-channel.js +94 -0
- package/lib/renderer/video-mask.js +115 -0
- package/lib/renderer/video-presentation.js +304 -39
- package/lib/renderer/video-worker.js +46 -0
- package/lib/renderer/workers.js +7 -17
- package/lib/style/computed-style.js +257 -61
- package/package.json +1 -1
- package/test/animation-frames.test.js +67 -0
- package/test/canvas-style-snapshot.test.js +18 -0
- package/test/cdp-stream.test.js +96 -3
- package/test/compositor-channel-ownership.test.js +194 -0
- package/test/compositor.test.js +27 -1
- package/test/computed-style-binding.test.js +49 -0
- package/test/computed-style-connectivity.test.js +13 -0
- package/test/computed-style.test.js +86 -0
- package/test/cookie-sync.test.js +82 -0
- package/test/document-base-cache.test.js +103 -0
- package/test/document-cookies.test.js +117 -0
- package/test/duplicate-style-keys.test.js +11 -0
- package/test/fixtures/animation-frames.js +56 -0
- package/test/fixtures/canvas-style-snapshot.js +81 -0
- package/test/fixtures/computed-style-connectivity.js +101 -0
- package/test/fixtures/computed-style-lazy.js +135 -0
- package/test/fixtures/duplicate-style-keys.js +97 -0
- package/test/fixtures/fake-renderer.js +12 -0
- package/test/fixtures/font-definition-invalidation.js +97 -0
- package/test/fixtures/font-metrics-invalidation.js +87 -0
- package/test/fixtures/full-style-identity.js +36 -0
- package/test/fixtures/hidden-style-engine.js +260 -0
- package/test/fixtures/hidden-style-reentry.js +232 -0
- package/test/fixtures/hidden-style-renderer.js +203 -0
- package/test/fixtures/layout-repaint.js +69 -0
- package/test/fixtures/media-presentation-continuation.js +141 -0
- package/test/fixtures/media-presentation-refill.js +113 -0
- package/test/fixtures/media-probe.js +148 -0
- package/test/fixtures/media-refill-cancel.js +79 -0
- package/test/fixtures/media-repaint.js +48 -0
- package/test/fixtures/media-resource-reselection.js +94 -0
- package/test/fixtures/media-source-attachment.js +136 -0
- package/test/fixtures/media-source-no-decoder.js +17 -0
- package/test/fixtures/media-track-refill.js +132 -0
- package/test/fixtures/media-webm-seek.js +51 -0
- package/test/fixtures/mutation-observer.js +201 -0
- package/test/fixtures/observer-invalidation.js +105 -0
- package/test/fixtures/prepaint-scheduling.js +73 -0
- package/test/fixtures/resource-discovery.js +32 -0
- package/test/fixtures/spare-renderer-scheduling.js +124 -0
- package/test/fixtures/style-propagation.js +26 -0
- package/test/fixtures/svg-node-reuse.js +83 -0
- package/test/fixtures/video-black-replay.js +140 -0
- package/test/fixtures/video-controls-invalidation.js +71 -0
- package/test/fixtures/viewport-bounds.js +148 -0
- package/test/font-loading.test.js +77 -0
- package/test/font-metrics-invalidation.test.js +60 -0
- package/test/full-style-identity.test.js +191 -0
- package/test/hidden-style-lazy.test.js +31 -0
- package/test/hidden-style-reentry.test.js +20 -0
- package/test/idle-callback.test.js +89 -0
- package/test/idle-scheduler.test.js +148 -0
- package/test/inline-style-cascade.test.js +63 -0
- package/test/layout-repaint-renderer.test.js +9 -0
- package/test/layout-repaint.test.js +222 -0
- package/test/media-presentation-continuation.test.js +14 -0
- package/test/media-presentation-refill.test.js +8 -0
- package/test/media-probe.test.js +16 -0
- package/test/media-repaint.test.js +8 -0
- package/test/media-source-attachment.test.js +34 -0
- package/test/media-track-refill.test.js +26 -0
- package/test/media-webm-seek.test.js +11 -0
- package/test/mp4-keyframes.test.js +36 -0
- package/test/mutation-observer.test.js +91 -0
- package/test/observer-invalidation.test.js +12 -0
- package/test/page-scripts.test.js +35 -0
- package/test/page-timers.test.js +67 -0
- package/test/paint-frame.test.js +18 -1
- package/test/pcm.test.js +63 -0
- package/test/prepaint-scheduling.test.js +13 -0
- package/test/pulse-client.test.js +165 -0
- package/test/raw-capture.test.js +34 -5
- package/test/renderer-host.test.js +51 -0
- package/test/resource-discovery.test.js +12 -0
- package/test/resource-elements.test.js +234 -0
- package/test/root-self-style.test.js +104 -0
- package/test/script-rewrite.test.js +35 -0
- package/test/seccomp.test.js +45 -0
- package/test/spare-renderer-scheduling.test.js +8 -0
- package/test/style-propagation.test.js +225 -0
- package/test/style-sheets.test.js +19 -0
- package/test/svg.test.js +45 -0
- package/test/timers.test.js +58 -0
- package/test/video-black-replay.test.js +34 -0
- package/test/video-capture.test.js +170 -0
- package/test/video-channel.test.js +253 -0
- package/test/video-continuation.test.js +57 -0
- package/test/video-controls-invalidation.test.js +12 -0
- package/test/video-keyframe.test.js +104 -4
- package/test/video-mask.test.js +94 -0
- package/test/video-presentation.test.js +239 -1
- package/test/video-timing-presentation.test.js +115 -0
- package/test/viewport-bounds.test.js +13 -0
- package/test/webm-snapshots.test.js +372 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cooperative idle work shares a short period, rather than granting every
|
|
3
|
+
* queued callback a fresh budget. Dispatch one callback per host task so
|
|
4
|
+
* ordinary timers and input can run between callbacks. A callback registered
|
|
5
|
+
* during a period joins the next one; positive timeouts have their own task.
|
|
6
|
+
* Clock/timer injection keeps the fairness and lifetime tests deterministic.
|
|
7
|
+
*/
|
|
8
|
+
export function createIdleCallbacks({ now, setTimer, clearTimer, invoke }) {
|
|
9
|
+
const callbacks = new Map();
|
|
10
|
+
let runnable = [], next = 0, deadline = 0, timer = null;
|
|
11
|
+
let running = false, disposed = false;
|
|
12
|
+
|
|
13
|
+
function queue() {
|
|
14
|
+
if (disposed) return;
|
|
15
|
+
if (!callbacks.size) {
|
|
16
|
+
if (timer !== null) clearTimer(timer);
|
|
17
|
+
timer = null;
|
|
18
|
+
runnable = []; next = 0;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (running || timer !== null) return;
|
|
22
|
+
// A nested request must not start a new period before this one ends.
|
|
23
|
+
const delay = next >= runnable.length ? Math.max(1, deadline - now()) : 1;
|
|
24
|
+
timer = setTimer(dispatch, delay);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function remove(record) {
|
|
28
|
+
if (callbacks.get(record.id) !== record) return false;
|
|
29
|
+
callbacks.delete(record.id);
|
|
30
|
+
if (record.timer !== null) clearTimer(record.timer);
|
|
31
|
+
record.timer = null;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function deliver(record, didTimeout, until) {
|
|
36
|
+
if (!remove(record)) return;
|
|
37
|
+
invoke(record.callback, {
|
|
38
|
+
get didTimeout() { return didTimeout; },
|
|
39
|
+
timeRemaining: () => Math.max(0, until - now()),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function queueTimeout(record) {
|
|
44
|
+
// Web IDL permits unsigned-long timeouts, while host timers only accept a
|
|
45
|
+
// signed 32-bit delay. Chunk longer waits instead of overflowing to 1ms.
|
|
46
|
+
record.timer = setTimer(() => {
|
|
47
|
+
record.timer = null;
|
|
48
|
+
if (disposed || callbacks.get(record.id) !== record) return;
|
|
49
|
+
if (now() < record.expires) { queueTimeout(record); return; }
|
|
50
|
+
running = true;
|
|
51
|
+
try { deliver(record, true, now()); }
|
|
52
|
+
finally {
|
|
53
|
+
running = false;
|
|
54
|
+
queue();
|
|
55
|
+
}
|
|
56
|
+
}, Math.min(2147483647, Math.max(1, record.expires - now())));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function dispatch() {
|
|
60
|
+
timer = null;
|
|
61
|
+
if (disposed || !callbacks.size) return;
|
|
62
|
+
running = true;
|
|
63
|
+
try {
|
|
64
|
+
const at = now();
|
|
65
|
+
if (at >= deadline) {
|
|
66
|
+
runnable = [...callbacks.values()];
|
|
67
|
+
next = 0;
|
|
68
|
+
deadline = at + 10;
|
|
69
|
+
}
|
|
70
|
+
while (next < runnable.length && callbacks.get(runnable[next].id) !== runnable[next]) next++;
|
|
71
|
+
if (next < runnable.length) {
|
|
72
|
+
const record = runnable[next++], at = now();
|
|
73
|
+
const didTimeout = at >= record.expires;
|
|
74
|
+
deliver(record, didTimeout, didTimeout ? at : deadline);
|
|
75
|
+
}
|
|
76
|
+
} finally {
|
|
77
|
+
running = false;
|
|
78
|
+
queue();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
request(id, callback, timeout) {
|
|
84
|
+
if (disposed) return id;
|
|
85
|
+
const record = { id, callback, expires: timeout > 0 ? now() + timeout : Infinity, timer: null };
|
|
86
|
+
callbacks.set(id, record);
|
|
87
|
+
if (timeout > 0) queueTimeout(record);
|
|
88
|
+
queue();
|
|
89
|
+
return id;
|
|
90
|
+
},
|
|
91
|
+
cancel(id) {
|
|
92
|
+
const record = callbacks.get(id);
|
|
93
|
+
if (record) remove(record);
|
|
94
|
+
if (!callbacks.size) {
|
|
95
|
+
if (timer !== null) clearTimer(timer);
|
|
96
|
+
timer = null;
|
|
97
|
+
runnable = []; next = 0;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
dispose() {
|
|
101
|
+
disposed = true;
|
|
102
|
+
if (timer !== null) clearTimer(timer);
|
|
103
|
+
timer = null;
|
|
104
|
+
for (const record of callbacks.values()) if (record.timer !== null) clearTimer(record.timer);
|
|
105
|
+
callbacks.clear();
|
|
106
|
+
runnable = []; next = 0;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
package/lib/renderer/main.js
CHANGED
|
@@ -265,7 +265,7 @@ const renderer = await createPageRenderer({
|
|
|
265
265
|
clipboardWriteInput:text=>clipboardWrite(text,'clipboard-write-input'),
|
|
266
266
|
webSockets,
|
|
267
267
|
audio,
|
|
268
|
-
setCookie: (value) => process.send({ type: "cookie", value }),
|
|
268
|
+
setCookie: (value, url, sequence) => process.send({ type: "cookie", value, url, sequence }),
|
|
269
269
|
reportError: (message) => report(`buninu-browser: page error: ${logging ? message : String(message).split("\n")[0]}`),
|
|
270
270
|
consoleMessage: logging ? (level, text) => report(`page console.${level}: ${text}`) : null,
|
|
271
271
|
trace: logging ? (text) => report(`timing: ${text}`) : null,
|
|
@@ -352,6 +352,10 @@ async function warmUp() {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
process.on("message", async (message) => {
|
|
355
|
+
if (message?.type === "cookie-state") {
|
|
356
|
+
renderer.updateCookies(message.state);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
355
359
|
if(message?.type==='clipboard-result') {
|
|
356
360
|
const pending=clipboardWrites.get(message.id);if(!pending)return;
|
|
357
361
|
clipboardWrites.delete(message.id);clearTimeout(pending.timer);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { createBytePipe } from "../media/byte-pipe.js";
|
|
16
16
|
import { createSourceSnapshots } from "../media/source-snapshots.js";
|
|
17
|
+
import { createWebMAudioSnapshots } from "../media/webm-snapshots.js";
|
|
17
18
|
import { createWebMStream, ebmlHeader } from "../media/webm-stream.js";
|
|
18
19
|
|
|
19
20
|
const BRAND = Symbol("MediaSource");
|
|
@@ -444,6 +445,8 @@ export function createMediaSourceScope({ origin, schedule, DOMException, onAppen
|
|
|
444
445
|
this._webm = {};
|
|
445
446
|
this._webmCovered = new Map();
|
|
446
447
|
this._webmStream = /^(video|audio)\/webm\b/i.test(this._mime) ? createWebMStream() : null;
|
|
448
|
+
this._webmSnapshots = /^audio\/webm\b/i.test(this._mime) ? createWebMAudioSnapshots() : null;
|
|
449
|
+
if (this._webmSnapshots) this._pipe.snapshotAt = time => this._webmSnapshots.at(time, () => this._pipe.bytes());
|
|
447
450
|
this._hasSegmentTimeline = false;
|
|
448
451
|
this._hasMediaSegment = false;
|
|
449
452
|
this._appendWindowStart = 0;
|
|
@@ -589,6 +592,7 @@ export function createMediaSourceScope({ origin, schedule, DOMException, onAppen
|
|
|
589
592
|
addCovered(this._covered, start, end);
|
|
590
593
|
}
|
|
591
594
|
}
|
|
595
|
+
this._webmSnapshots?.append({ id, bytes, cluster });
|
|
592
596
|
if (cluster && this._webmCluster !== cluster.token) {
|
|
593
597
|
for (const prefix of cluster.prefix) this._pipe.append(prefix);
|
|
594
598
|
this._webmCluster = cluster.token;
|
|
@@ -22,10 +22,12 @@ import {
|
|
|
22
22
|
rewriteClassicScriptSource, rewriteConstructorReads, rewriteModuleSource,
|
|
23
23
|
} from "./script-rewrite.js";
|
|
24
24
|
import { prepareClassicScript } from "./prepared-script-cache.js";
|
|
25
|
-
import { mediaQueryMatches, resolveVariables } from "../style/computed-style.js";
|
|
25
|
+
import { computeElementStyle, mediaQueryMatches, resolveVariables } from "../style/computed-style.js";
|
|
26
26
|
import { asIncumbent, createMessaging } from "./messaging.js";
|
|
27
27
|
import { canvasContext, resetCanvas, disposeDocumentCanvases, encodeCanvas } from "./canvas-2d.js";
|
|
28
28
|
import { createWorkers } from "./workers.js";
|
|
29
|
+
import { createTimers } from "./timers.js";
|
|
30
|
+
import { createAnimationFrames, supportsFrameCheckpoint } from "./animation-frames.js";
|
|
29
31
|
import { createMediaSourceScope } from "./media-source.js";
|
|
30
32
|
import { markNative, markNativeApi, setFunctionSourceTextTransform } from "./native-functions.js";
|
|
31
33
|
import { applyGlobalShape, errorInterface, platformObject, storageInterface } from "./interfaces.js";
|
|
@@ -33,6 +35,7 @@ import { trustedTypesApi } from "./trusted-types.js";
|
|
|
33
35
|
import { onPromiseRejection } from "./rejections.js";
|
|
34
36
|
import { installAttributeReactions } from "./custom-element-reactions.js";
|
|
35
37
|
import { createLayoutObservers } from "./layout-observers.js";
|
|
38
|
+
import { createIdleCallbacks } from "./idle-callbacks.js";
|
|
36
39
|
import { createBroadcastChannels } from "./broadcast-channel.js";
|
|
37
40
|
import { createInertWindow } from "../happy-dom/parser.js";
|
|
38
41
|
import { historyBinding } from "./history.js";
|
|
@@ -257,8 +260,8 @@ function cssEscape(value) {
|
|
|
257
260
|
* boundsOf(element: object): { left: number, top: number, width: number, height: number } | null,
|
|
258
261
|
* viewport(): { width: number, height: number, scrollX: number, scrollY: number, devicePixelRatio: number },
|
|
259
262
|
* scrollTo(x: number, y: number): void,
|
|
260
|
-
* computedStyle(element: object): object | null,
|
|
261
|
-
* customProperties?(element: object): Map<string, string> | null,
|
|
263
|
+
* computedStyle(element: object, needsLayout?: boolean): object | null,
|
|
264
|
+
* customProperties?(element: object, updateStyle?: boolean): Map<string, string> | null,
|
|
262
265
|
* navigate(url: string): void,
|
|
263
266
|
* activateLink?(anchor: object): void,
|
|
264
267
|
* fetchScript(url: string): Promise<string>,
|
|
@@ -271,7 +274,6 @@ function cssEscape(value) {
|
|
|
271
274
|
* }} options
|
|
272
275
|
*/
|
|
273
276
|
export function createPageRealm({ window, document, mode, hooks }) {
|
|
274
|
-
const timers = new Map();
|
|
275
277
|
// Close functions of this document's open WebSockets (see networkApi)
|
|
276
278
|
const openSockets = new Set();
|
|
277
279
|
// Happy DOM runs its own document lifecycle and fires load/DOMContentLoaded
|
|
@@ -372,6 +374,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
372
374
|
playMedia: hooks.playMedia,
|
|
373
375
|
pauseMedia: hooks.pauseMedia,
|
|
374
376
|
seekMedia: hooks.seekMedia,
|
|
377
|
+
reselectMedia: hooks.reselectMedia,
|
|
375
378
|
attachMediaSrc: (element, value) => hooks.attachMediaSrc?.(element, value),
|
|
376
379
|
mediaReadyState: (element) => hooks.mediaReadyState?.(element) ?? 0,
|
|
377
380
|
mediaNetworkState: (element) => hooks.mediaNetworkState?.(element) ?? 0,
|
|
@@ -742,25 +745,23 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
742
745
|
target.dispatchEvent(event);
|
|
743
746
|
};
|
|
744
747
|
|
|
745
|
-
const
|
|
746
|
-
|
|
748
|
+
const timers = createTimers({ invoke: callback => runTask(callback), nextId: () => nextTimerId++ });
|
|
749
|
+
const schedule = (handler, delay, args, repeat, timerTask = true) => {
|
|
750
|
+
const code = typeof handler === "function" ? null : String(handler);
|
|
747
751
|
const callback = typeof handler === "function"
|
|
748
752
|
? () => callPage(handler, pageGlobal, args)
|
|
749
|
-
: () => evaluate(
|
|
750
|
-
|
|
751
|
-
if (!timers.has(id)) return;
|
|
752
|
-
if (repeat) timers.set(id, hostSetTimeout(fire, Math.max(4, delay)));
|
|
753
|
-
else timers.delete(id);
|
|
754
|
-
runTask(callback);
|
|
755
|
-
};
|
|
756
|
-
timers.set(id, hostSetTimeout(fire, Math.max(0, Number(delay) || 0)));
|
|
757
|
-
return id;
|
|
758
|
-
};
|
|
759
|
-
const cancel = (id) => {
|
|
760
|
-
const handle = timers.get(Number(id));
|
|
761
|
-
if (handle) hostClearTimeout(handle);
|
|
762
|
-
timers.delete(Number(id));
|
|
753
|
+
: () => evaluate(code, "timer");
|
|
754
|
+
return timers.schedule(callback, delay, repeat, timerTask);
|
|
763
755
|
};
|
|
756
|
+
const cancel = id => timers.cancel(id);
|
|
757
|
+
const animationFrames = supportsFrameCheckpoint() ? createAnimationFrames({
|
|
758
|
+
invoke: (callback, timestamp) => runTask(() => callPage(callback, pageGlobal, [timestamp])),
|
|
759
|
+
now: pageNow,
|
|
760
|
+
}) : null;
|
|
761
|
+
const idleCallbacks = createIdleCallbacks({
|
|
762
|
+
now: () => hostPerformance.now(), setTimer: hostSetTimeout, clearTimer: hostClearTimeout,
|
|
763
|
+
invoke: (callback, deadline) => runTask(() => callPage(callback, pageGlobal, [deadline])),
|
|
764
|
+
});
|
|
764
765
|
|
|
765
766
|
const pageConsole = Object.fromEntries(["log", "info", "warn", "error", "debug", "trace"].map((level) => [
|
|
766
767
|
level,
|
|
@@ -801,13 +802,14 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
801
802
|
|
|
802
803
|
const layoutObservers = createLayoutObservers({
|
|
803
804
|
nodeOf, boundsOf: hooks.boundsOf, computedStyle: hooks.computedStyle, viewport: hooks.viewport,
|
|
804
|
-
schedule: (callback) => schedule(callback, 0, [], false), cancel, call: callPage, now: pageNow,
|
|
805
|
+
schedule: (callback) => schedule(callback, 0, [], false, false), cancel, call: callPage, now: pageNow,
|
|
805
806
|
});
|
|
806
807
|
|
|
807
808
|
/** MutationObserver wraps Happy DOM records; layout observers use renderer geometry. */
|
|
808
809
|
function observerConstructors() {
|
|
809
810
|
function MutationObserver(callback) {
|
|
810
811
|
if (!new.target) throw new TypeError("Constructor MutationObserver requires 'new'");
|
|
812
|
+
if (typeof callback !== "function") throw new TypeError("MutationObserver callback must be a function");
|
|
811
813
|
const self = {};
|
|
812
814
|
const wrapRecord = (record) => ({
|
|
813
815
|
type: record.type,
|
|
@@ -825,15 +827,22 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
825
827
|
self.observe = (target, options = {}) => {
|
|
826
828
|
const node = nodeOf(target);
|
|
827
829
|
if (!node) throw new TypeError("MutationObserver.observe requires a Node");
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
}
|
|
830
|
+
const normalized = {};
|
|
831
|
+
const filter = options?.attributeFilter;
|
|
832
|
+
if (filter !== undefined) {
|
|
833
|
+
if (filter === null || (typeof filter !== "object" && typeof filter !== "function")) {
|
|
834
|
+
throw new TypeError("MutationObserver attributeFilter must be iterable");
|
|
835
|
+
}
|
|
836
|
+
normalized.attributeFilter = [];
|
|
837
|
+
for (const name of filter) normalized.attributeFilter.push(`${name}`);
|
|
838
|
+
}
|
|
839
|
+
// Omitted boolean members carry different semantics from false: e.g.
|
|
840
|
+
// attributeOldValue:false with no attributes member enables attributes.
|
|
841
|
+
for (const name of ["attributeOldValue", "attributes", "characterData", "characterDataOldValue", "childList", "subtree"]) {
|
|
842
|
+
const value = options?.[name];
|
|
843
|
+
if (value !== undefined) normalized[name] = Boolean(value);
|
|
844
|
+
}
|
|
845
|
+
real.observe(node, normalized);
|
|
837
846
|
};
|
|
838
847
|
self.disconnect = () => real.disconnect();
|
|
839
848
|
self.takeRecords = () => real.takeRecords().map(wrapRecord);
|
|
@@ -1642,11 +1651,21 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1642
1651
|
// Each is its own function with its Web IDL length (handles are optional for the clears).
|
|
1643
1652
|
clearTimeout: (id = undefined) => cancel(id),
|
|
1644
1653
|
clearInterval: (id = undefined) => cancel(id),
|
|
1645
|
-
requestAnimationFrame: (callback) =>
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1654
|
+
requestAnimationFrame: (callback) => animationFrames
|
|
1655
|
+
? animationFrames.request(callback)
|
|
1656
|
+
: schedule(() => callPage(callback, pageGlobal, [pageNow()]), 16, [], false, false),
|
|
1657
|
+
cancelAnimationFrame: (handle) => animationFrames ? animationFrames.cancel(handle) : cancel(handle),
|
|
1658
|
+
requestIdleCallback: (callback, options = undefined) => {
|
|
1659
|
+
if (typeof callback !== "function") throw new TypeError("requestIdleCallback: callback is not a function");
|
|
1660
|
+
if (options != null && typeof options !== "object" && typeof options !== "function") {
|
|
1661
|
+
throw new TypeError("requestIdleCallback: options must be a dictionary");
|
|
1662
|
+
}
|
|
1663
|
+
// IdleRequestOptions.timeout is an unsigned long; zero does not start
|
|
1664
|
+
// a timeout. Idle callbacks share short periods and yield between tasks.
|
|
1665
|
+
const timeout = options?.timeout >>> 0;
|
|
1666
|
+
return idleCallbacks.request(nextTimerId++, callback, timeout);
|
|
1667
|
+
},
|
|
1668
|
+
cancelIdleCallback: (handle) => idleCallbacks.cancel(Number(handle)),
|
|
1650
1669
|
queueMicrotask: (callback) => hostQueueMicrotask(() => runTask(() => callPage(callback, pageGlobal, []))),
|
|
1651
1670
|
structuredClone: (value) => hostStructuredClone(value),
|
|
1652
1671
|
atob: (value) => hostAtob(String(value)),
|
|
@@ -1781,11 +1800,17 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
1781
1800
|
dispatchEvent: windowEvents.dispatchEvent,
|
|
1782
1801
|
getComputedStyle: (element) => {
|
|
1783
1802
|
const node = nodeOf(element);
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
element: node
|
|
1803
|
+
return computedStyleBinding((needsLayout) => {
|
|
1804
|
+
// Detached elements have no resolved values, even if the style engine
|
|
1805
|
+
// still retains their last connected style. Check every live read so
|
|
1806
|
+
// an existing declaration also empties on removal and revives on attach.
|
|
1807
|
+
if (!node?.isConnected) return { style: null, custom: null, bounds: null, element: node };
|
|
1808
|
+
const style = hooks.computedStyle(node, needsLayout);
|
|
1809
|
+
return {
|
|
1810
|
+
style, custom: hooks.customProperties?.(node, false) ?? null,
|
|
1811
|
+
bounds: needsLayout && style && style.display !== "none" && node?.isConnected ? hooks.boundsOf(node) : null,
|
|
1812
|
+
element: node,
|
|
1813
|
+
};
|
|
1789
1814
|
});
|
|
1790
1815
|
},
|
|
1791
1816
|
getSelection: () => realm.selection(),
|
|
@@ -2454,8 +2479,9 @@ export function createPageRealm({ window, document, mode, hooks }) {
|
|
|
2454
2479
|
// Leaving the document closes its WebSockets (1001 "going away").
|
|
2455
2480
|
for (const close of openSockets) close();
|
|
2456
2481
|
openSockets.clear();
|
|
2457
|
-
|
|
2458
|
-
|
|
2482
|
+
timers.dispose();
|
|
2483
|
+
animationFrames?.dispose();
|
|
2484
|
+
idleCallbacks.dispose();
|
|
2459
2485
|
},
|
|
2460
2486
|
};
|
|
2461
2487
|
}
|
|
@@ -2856,7 +2882,78 @@ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile
|
|
|
2856
2882
|
};
|
|
2857
2883
|
}
|
|
2858
2884
|
|
|
2859
|
-
|
|
2885
|
+
// CSSOM returns a live declaration. Like Blink's CSSComputedStyleDeclaration,
|
|
2886
|
+
// resolve style first and update layout only for layout-dependent properties.
|
|
2887
|
+
const COMPUTED_LAYOUT_PROPERTIES = new Set([
|
|
2888
|
+
"width", "height", "top", "right", "bottom", "left", "transform",
|
|
2889
|
+
"margin", "marginTop", "marginRight", "marginBottom", "marginLeft",
|
|
2890
|
+
"padding", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft",
|
|
2891
|
+
]);
|
|
2892
|
+
const COMPUTED_PADDING_PROPERTIES = new Map([
|
|
2893
|
+
["padding", -1], ["paddingTop", 0], ["paddingRight", 1], ["paddingBottom", 2], ["paddingLeft", 3],
|
|
2894
|
+
]);
|
|
2895
|
+
// Populated once from the snapshot's fixed internal fields, never from page
|
|
2896
|
+
// input. Reuse the CSS/JS spelling table across elements and live snapshots.
|
|
2897
|
+
const COMPUTED_CSS_PROPERTIES = new Map();
|
|
2898
|
+
let computedStylePropertyNames = null;
|
|
2899
|
+
let computedStyleAccessorNames = null;
|
|
2900
|
+
const COMPUTED_STYLE_SCHEMA_ELEMENT = Object.freeze({
|
|
2901
|
+
tagName: "DIV", getAttribute: () => null, hasAttribute: () => false,
|
|
2902
|
+
});
|
|
2903
|
+
// Computed styles and custom-property maps retain their identities until
|
|
2904
|
+
// restyle changes them. Share serialization across repeated declarations;
|
|
2905
|
+
// used geometry remains local to each live declaration and its bounds.
|
|
2906
|
+
const computedStyleSnapshots = new WeakMap();
|
|
2907
|
+
|
|
2908
|
+
function computedStyleBinding(read) {
|
|
2909
|
+
const initial = read(false);
|
|
2910
|
+
const shape = computedStyleSnapshot(initial.style, initial);
|
|
2911
|
+
// A declaration first obtained while detached still exposes the ordinary
|
|
2912
|
+
// named accessors. Derive their schema once from the same serializer, without
|
|
2913
|
+
// resolving the detached node or duplicating its supported-property list.
|
|
2914
|
+
if (computedStyleAccessorNames === null) {
|
|
2915
|
+
computedStyleSnapshot(computeElementStyle(COMPUTED_STYLE_SCHEMA_ELEMENT));
|
|
2916
|
+
}
|
|
2917
|
+
const snapshots = [{ ...initial, value: shape }, null];
|
|
2918
|
+
const snapshot = name => {
|
|
2919
|
+
const property = name.startsWith("--") ? null : COMPUTED_CSS_PROPERTIES.get(name) ?? name;
|
|
2920
|
+
let layout = COMPUTED_LAYOUT_PROPERTIES.has(property);
|
|
2921
|
+
let state;
|
|
2922
|
+
const side = COMPUTED_PADDING_PROPERTIES.get(property);
|
|
2923
|
+
if (side !== undefined) {
|
|
2924
|
+
state = read(false);
|
|
2925
|
+
const padding = state.style?.padding;
|
|
2926
|
+
// CSSOM resolves fixed padding directly from computed style. Percentages
|
|
2927
|
+
// and unresolved math still need the containing block's used geometry.
|
|
2928
|
+
if (padding && (side < 0 ? padding.every(value => typeof value === "number")
|
|
2929
|
+
: typeof padding[side] === "number")) layout = false;
|
|
2930
|
+
}
|
|
2931
|
+
if (!state || layout) state = read(layout);
|
|
2932
|
+
const prior = snapshots[Number(layout)];
|
|
2933
|
+
const bounds = state.bounds;
|
|
2934
|
+
if (prior && prior.style === state.style && prior.custom === state.custom
|
|
2935
|
+
&& (prior.bounds === bounds || (prior.bounds && bounds
|
|
2936
|
+
&& prior.bounds.width === bounds.width && prior.bounds.height === bounds.height))) return prior.value;
|
|
2937
|
+
const value = computedStyleSnapshot(state.style, state);
|
|
2938
|
+
snapshots[Number(layout)] = { ...state, value };
|
|
2939
|
+
return value;
|
|
2940
|
+
};
|
|
2941
|
+
const declaration = {
|
|
2942
|
+
get length() { return snapshot("").length; },
|
|
2943
|
+
item(index) { return snapshot("").item(index); },
|
|
2944
|
+
getPropertyValue(name) {
|
|
2945
|
+
const key = String(name);
|
|
2946
|
+
return snapshot(key.startsWith("--") ? key : key.toLowerCase()).getPropertyValue(key);
|
|
2947
|
+
},
|
|
2948
|
+
getPropertyPriority() { return ""; },
|
|
2949
|
+
get cssText() { return ""; },
|
|
2950
|
+
};
|
|
2951
|
+
for (const name of computedStyleAccessorNames) Object.defineProperty(declaration, name, {
|
|
2952
|
+
enumerable: true, configurable: true, get: () => snapshot(name)[name] ?? "",
|
|
2953
|
+
});
|
|
2954
|
+
return declaration;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2860
2957
|
/**
|
|
2861
2958
|
* The page's CSSStyleDeclaration for getComputedStyle (CSSOM §9): resolved
|
|
2862
2959
|
* values for the longhands the engine computes, by camelCase name and by
|
|
@@ -2864,7 +2961,10 @@ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile
|
|
|
2864
2961
|
* are used values (layout's box) when the element is rendered; percentages
|
|
2865
2962
|
* that depend on layout read as specified otherwise.
|
|
2866
2963
|
*/
|
|
2867
|
-
function
|
|
2964
|
+
function computedStyleSnapshot(style, { custom = null, bounds = null, element = null } = {}) {
|
|
2965
|
+
const reusable = !bounds && style && Object.isFrozen(style);
|
|
2966
|
+
const cached = reusable && computedStyleSnapshots.get(style);
|
|
2967
|
+
if (cached && cached.custom === custom) return cached.value;
|
|
2868
2968
|
const px = (value) => `${Math.round(value * 1000) / 1000}px`;
|
|
2869
2969
|
const length = (value) => typeof value === "number" ? px(value)
|
|
2870
2970
|
: value?.unit === "%" ? `${value.value}%`
|
|
@@ -2975,14 +3075,20 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
|
|
|
2975
3075
|
values.margin = sides.map((side) => values[`margin${side}`]).join(" ");
|
|
2976
3076
|
values.padding = sides.map((side) => values[`padding${side}`]).join(" ");
|
|
2977
3077
|
}
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3078
|
+
if (style && computedStylePropertyNames === null) {
|
|
3079
|
+
computedStyleAccessorNames = [...Object.keys(values), "webkitLineClamp", "webkitBoxOrient"];
|
|
3080
|
+
computedStylePropertyNames = Object.keys(values).map(name => {
|
|
3081
|
+
const cssName = name.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
|
|
3082
|
+
COMPUTED_CSS_PROPERTIES.set(cssName, name);
|
|
3083
|
+
return cssName;
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
const names = style ? computedStylePropertyNames : [];
|
|
2981
3087
|
const customValue = (name) => {
|
|
2982
3088
|
const value = custom?.get(name);
|
|
2983
3089
|
return value == null ? "" : resolveVariables(value, custom);
|
|
2984
3090
|
};
|
|
2985
|
-
|
|
3091
|
+
const result = {
|
|
2986
3092
|
...values,
|
|
2987
3093
|
webkitLineClamp: values.WebkitLineClamp,
|
|
2988
3094
|
webkitBoxOrient: values.WebkitBoxOrient,
|
|
@@ -2990,13 +3096,17 @@ function computedStyleBinding(style, { custom = null, bounds = null, element = n
|
|
|
2990
3096
|
item: (index) => names[Number(index)] ?? "",
|
|
2991
3097
|
getPropertyValue: (name) => {
|
|
2992
3098
|
const key = String(name);
|
|
2993
|
-
|
|
3099
|
+
if (key.startsWith("--")) return customValue(key);
|
|
3100
|
+
const property = COMPUTED_CSS_PROPERTIES.get(key.toLowerCase());
|
|
3101
|
+
return property === undefined ? "" : values[property] ?? "";
|
|
2994
3102
|
},
|
|
2995
3103
|
getPropertyPriority: () => "",
|
|
2996
3104
|
get cssText() {
|
|
2997
3105
|
return "";
|
|
2998
3106
|
},
|
|
2999
3107
|
};
|
|
3108
|
+
if (reusable) computedStyleSnapshots.set(style, { custom, value: result });
|
|
3109
|
+
return result;
|
|
3000
3110
|
}
|
|
3001
3111
|
|
|
3002
3112
|
function formatConsoleValue(value) {
|