buninu-browser 0.2.1 → 0.2.2

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 (72) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +74 -9
  3. package/bunfig.toml +1 -0
  4. package/buninu-browser.js +4 -0
  5. package/index.js +5 -0
  6. package/lib/audio/piano.js +24 -1
  7. package/lib/audio/pulse-client.js +120 -12
  8. package/lib/cdp-server.js +16 -11
  9. package/lib/happy-dom/parser.js +24 -4
  10. package/lib/headless-shell.js +66 -19
  11. package/lib/input/hit-test.js +25 -7
  12. package/lib/layout/text-layout.js +196 -49
  13. package/lib/log.js +5 -2
  14. package/lib/media/byte-pipe.js +82 -0
  15. package/lib/media/position-audio.js +42 -0
  16. package/lib/network/network-service.js +161 -14
  17. package/lib/paint/display-list.js +300 -14
  18. package/lib/paint/gradient.js +11 -5
  19. package/lib/render-tree/index.js +17 -2
  20. package/lib/render-tree/svg.js +2 -1
  21. package/lib/renderer/bindings.js +985 -68
  22. package/lib/renderer/canvas-2d.js +92 -0
  23. package/lib/renderer/host.js +158 -8
  24. package/lib/renderer/interfaces.js +215 -0
  25. package/lib/renderer/main.js +160 -12
  26. package/lib/renderer/media-source.js +819 -0
  27. package/lib/renderer/messaging.js +411 -0
  28. package/lib/renderer/native-functions.js +113 -0
  29. package/lib/renderer/page-realm.js +1405 -198
  30. package/lib/renderer/page-renderer.js +1736 -193
  31. package/lib/renderer/rejections.js +31 -0
  32. package/lib/renderer/rewrite-worker.js +22 -0
  33. package/lib/renderer/script-rewrite.js +805 -57
  34. package/lib/renderer/trusted-types.js +129 -0
  35. package/lib/renderer/workers.js +327 -0
  36. package/lib/screenshot-cli.js +2 -0
  37. package/lib/style/computed-style.js +162 -22
  38. package/lib/user-agent.js +14 -2
  39. package/package.json +4 -1
  40. package/patches/happy-dom/0002-use-bun-platform-adapters.patch +139 -19
  41. package/patches/happy-dom/0006-parse-css-escapes-in-selectors.patch +115 -0
  42. package/patches/happy-dom/README.md +26 -0
  43. package/patches/ses/0002-embedder-native-functions-source-text-sloppy-code.patch +106 -0
  44. package/patches/ses/0003-cache-sloppy-evaluators.patch +47 -0
  45. package/patches/ses/README.md +4 -0
  46. package/test/canvas-2d.test.js +55 -0
  47. package/test/compositor.test.js +3 -1
  48. package/test/computed-style.test.js +38 -1
  49. package/test/display-list.test.js +74 -1
  50. package/test/download.test.js +35 -0
  51. package/test/fixtures/long-video.mp4 +0 -0
  52. package/test/fixtures/media-sync.js +57 -0
  53. package/test/happy-dom-platform-adapters.test.js +25 -0
  54. package/test/headless-shell.test.js +1 -1
  55. package/test/hit-test.test.js +25 -1
  56. package/test/media-source.test.js +156 -0
  57. package/test/media-sync.test.js +12 -0
  58. package/test/mobile.test.js +2 -2
  59. package/test/navigation.test.js +61 -0
  60. package/test/network-service.test.js +14 -0
  61. package/test/page-scripts.test.js +1045 -7
  62. package/test/position-audio.test.js +67 -0
  63. package/test/positioning.test.js +95 -1
  64. package/test/pulse-client.test.js +69 -0
  65. package/test/renderer-host.test.js +5 -2
  66. package/test/screenshot-cli.test.js +25 -1
  67. package/test/script-rewrite.test.js +111 -6
  68. package/test/setup.js +5 -0
  69. package/test/user-agent.test.js +60 -4
  70. package/vendor/happy-dom/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts +52 -22
  71. package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +18 -17
  72. package/vendor/ses/dist/ses.mjs +52 -6
@@ -12,11 +12,45 @@
12
12
  * the renderer schedule style, layout and paint.
13
13
  */
14
14
  import {
15
- BindingRealm, defineEventHandlerProperty, eventConstructors, eventTargetConstructor, hardenBindings, INTERFACES, mutableBindingPrototype, NodeFilter, nodeOf, windowEventMethods,
15
+ BindingRealm, cssStyleDeclarationInterface, defineEventHandlerProperty, eventConstructors, eventTargetConstructor, hardenBindings, INTERFACES, mutableBindingPrototype, NodeFilter, nodeOf, rangeConstructor, windowEventMethods,
16
16
  } from "./bindings.js";
17
17
  import { resolveModuleSpecifier, transformModule } from "./module-transform.js";
18
- import { CONSTRUCTOR_HELPER, rewriteClassicScript, rewriteConstructorReads } from "./script-rewrite.js";
19
- import { mediaQueryMatches } from "../style/computed-style.js";
18
+ import {
19
+ CONSTRUCTOR_HELPER, DIRECT_EVAL_HELPER, DIRECT_EVAL_STRICT, escapeSesHtmlComments, prepareDirectEvalSource,
20
+ DIRECT_EVAL_SCOPE_HELPER, REFERENCE_HELPER, referenceHelper, restoreSourceText, THIS_HELPER,
21
+ rewriteClassicScriptSource, rewriteConstructorReads, rewriteModuleSource,
22
+ } from "./script-rewrite.js";
23
+ import { mediaQueryMatches, resolveVariables } from "../style/computed-style.js";
24
+ import { asIncumbent, createMessaging } from "./messaging.js";
25
+ import { canvasContext, resetCanvas } from "./canvas-2d.js";
26
+ import { createWorkers } from "./workers.js";
27
+ import { createMediaSourceScope } from "./media-source.js";
28
+ import { markNative, markNativeApi, setFunctionSourceTextTransform } from "./native-functions.js";
29
+ import { applyGlobalShape, platformObject, storageInterface } from "./interfaces.js";
30
+ import { trustedTypesApi } from "./trusted-types.js";
31
+ import { onPromiseRejection } from "./rejections.js";
32
+ import { AsyncLocalStorage } from "node:async_hooks";
33
+
34
+ // URL is one shared intrinsic, hardened before any page sees it. Blob URL
35
+ // creation is installed once and dispatched to the document whose task is
36
+ // running (AsyncLocalStorage, with the latest document as a fallback).
37
+ const blobUrlContext = new AsyncLocalStorage();
38
+ let blobUrlFallback = null;
39
+
40
+ function installBlobUrls() {
41
+ const createObjectURL = function createObjectURL(object) {
42
+ const scope = blobUrlContext.getStore() ?? blobUrlFallback;
43
+ if (!scope) throw new TypeError("URL.createObjectURL is not available");
44
+ return scope.create(object);
45
+ };
46
+ const revokeObjectURL = function revokeObjectURL(url) {
47
+ const scope = blobUrlContext.getStore() ?? blobUrlFallback;
48
+ scope?.revoke(url);
49
+ };
50
+ Object.defineProperty(URL, "createObjectURL", { value: createObjectURL, writable: true, configurable: true });
51
+ Object.defineProperty(URL, "revokeObjectURL", { value: revokeObjectURL, writable: true, configurable: true });
52
+ }
53
+ installBlobUrls();
20
54
 
21
55
  // Captured at load, before host mode can install page globals over them.
22
56
  const hostSetTimeout = globalThis.setTimeout;
@@ -37,6 +71,44 @@ const INLINE_HANDLER_EVENTS = [
37
71
  "keypress", "beforeinput", "focus", "blur", "load",
38
72
  ];
39
73
  const MAX_CONSOLE_ENTRIES = 500;
74
+ /**
75
+ * Reflected content attributes (HTML §2.6.1) the engine does not implement
76
+ * itself, per interface: name → [content attribute, "string" | "unsigned"].
77
+ */
78
+ /** HTMLElement's own members (HTML §3.2.8), besides its on* event handlers. */
79
+ const HTML_ELEMENT_MEMBERS = new Set(["title", "lang", "translate", "dir", "hidden", "inert", "accessKey", "draggable",
80
+ "spellcheck", "autocapitalize", "contentEditable", "enterKeyHint", "isContentEditable", "inputMode", "popover",
81
+ "offsetParent", "offsetTop", "offsetLeft", "offsetWidth", "offsetHeight", "innerText", "outerText", "click",
82
+ "attachInternals", "showPopover", "hidePopover", "togglePopover", "dataset", "nonce", "autofocus", "tabIndex",
83
+ "style", "focus", "blur"]);
84
+
85
+ const REFLECTED_ATTRIBUTES = {
86
+ HTMLImageElement: { name: ["name", "string"], lowsrc: ["lowsrc", "string"], align: ["align", "string"],
87
+ longDesc: ["longdesc", "string"], border: ["border", "string"], hspace: ["hspace", "unsigned"],
88
+ vspace: ["vspace", "unsigned"], fetchPriority: ["fetchpriority", "string"] },
89
+ };
90
+ /** Reflected attributes whose IDL type is a nullable string (HTML §2.6.1). */
91
+ const NULLABLE_STRING_ATTRIBUTES = new Set(["crossOrigin"]);
92
+ // Page functions print the source the page wrote, not the engine's rewrite of it.
93
+ setFunctionSourceTextTransform(restoreSourceText);
94
+ /**
95
+ * Language globals every browser has that SES leaves out of compartments
96
+ * (float arrays for the NaN side channel, WeakRef and FinalizationRegistry
97
+ * for non-determinism, Atomics without SharedArrayBuffer, WebAssembly).
98
+ * SharedArrayBuffer stays out: Chromium only exposes it to cross-origin
99
+ * isolated documents.
100
+ */
101
+ const HOST_LANGUAGE_GLOBALS = Object.fromEntries(["Float16Array", "Float32Array", "Float64Array", "WeakRef",
102
+ "FinalizationRegistry", "Atomics", "WebAssembly"].filter((name) => globalThis[name] !== undefined)
103
+ .map((name) => [name, globalThis[name]]));
104
+ /** Streams (WHATWG Streams, Compression Streams, Encoding streams), Bun's own implementations. */
105
+ const HOST_STREAM_GLOBALS = Object.fromEntries(["ReadableStream", "ReadableStreamDefaultReader",
106
+ "ReadableStreamBYOBReader", "ReadableStreamDefaultController", "ReadableByteStreamController",
107
+ "ReadableStreamBYOBRequest", "WritableStream", "WritableStreamDefaultWriter",
108
+ "WritableStreamDefaultController", "TransformStream", "TransformStreamDefaultController",
109
+ "ByteLengthQueuingStrategy", "CountQueuingStrategy", "TextEncoderStream", "TextDecoderStream",
110
+ "CompressionStream", "DecompressionStream"].filter((name) => typeof globalThis[name] === "function")
111
+ .map((name) => [name, globalThis[name]]));
40
112
  // Element interfaces recognised by instanceof, by tag name.
41
113
  const ELEMENT_INTERFACES = Object.freeze({
42
114
  HTMLAnchorElement: ["A"], HTMLAreaElement: ["AREA"], HTMLAudioElement: ["AUDIO"], HTMLBRElement: ["BR"],
@@ -48,20 +120,83 @@ const ELEMENT_INTERFACES = Object.freeze({
48
120
  HTMLLabelElement: ["LABEL"], HTMLLegendElement: ["LEGEND"], HTMLLinkElement: ["LINK"], HTMLMapElement: ["MAP"],
49
121
  HTMLMediaElement: ["AUDIO", "VIDEO"], HTMLMetaElement: ["META"], HTMLMeterElement: ["METER"],
50
122
  HTMLOListElement: ["OL"], HTMLObjectElement: ["OBJECT"], HTMLOptGroupElement: ["OPTGROUP"], HTMLOptionElement: ["OPTION"],
51
- HTMLOutputElement: ["OUTPUT"], HTMLParagraphElement: ["P"], HTMLPictureElement: ["PICTURE"], HTMLPreElement: ["PRE"],
123
+ HTMLOutputElement: ["OUTPUT"], HTMLParagraphElement: ["P"], HTMLPictureElement: ["PICTURE"], HTMLPreElement: ["PRE", "LISTING", "XMP"],
52
124
  HTMLProgressElement: ["PROGRESS"], HTMLQuoteElement: ["BLOCKQUOTE", "Q"], HTMLScriptElement: ["SCRIPT"],
53
125
  HTMLSelectElement: ["SELECT"], HTMLSlotElement: ["SLOT"], HTMLSourceElement: ["SOURCE"], HTMLSpanElement: ["SPAN"],
54
126
  HTMLStyleElement: ["STYLE"], HTMLTableCellElement: ["TD", "TH"], HTMLTableElement: ["TABLE"], HTMLTableRowElement: ["TR"],
55
127
  HTMLTableSectionElement: ["THEAD", "TBODY", "TFOOT"], HTMLTemplateElement: ["TEMPLATE"], HTMLTextAreaElement: ["TEXTAREA"],
56
128
  HTMLTimeElement: ["TIME"], HTMLTitleElement: ["TITLE"], HTMLTrackElement: ["TRACK"], HTMLUListElement: ["UL"],
57
- HTMLVideoElement: ["VIDEO"],
129
+ HTMLVideoElement: ["VIDEO"], HTMLDataElement: ["DATA"], HTMLModElement: ["INS", "DEL"], HTMLParamElement: ["PARAM"],
130
+ HTMLTableCaptionElement: ["CAPTION"], HTMLTableColElement: ["COL", "COLGROUP"], HTMLDataListElement: ["DATALIST"],
131
+ HTMLMenuElement: ["MENU"], HTMLFontElement: ["FONT"], HTMLDirectoryElement: ["DIR"], HTMLFrameElement: ["FRAME"],
132
+ HTMLFrameSetElement: ["FRAMESET"], HTMLMarqueeElement: ["MARQUEE"],
58
133
  });
134
+ /**
135
+ * HTML elements whose interface is HTMLElement itself (HTML §4 element index,
136
+ * and §16's obsolete elements). Any other name in the HTML namespace that is
137
+ * not a valid custom element name is an HTMLUnknownElement (DOM "element
138
+ * interface"), as <lh> or <blink> are.
139
+ */
140
+ const PLAIN_HTML_ELEMENTS = new Set(["ABBR", "ADDRESS", "ARTICLE", "ASIDE", "B", "BDI", "BDO", "CITE", "CODE", "DD", "DFN",
141
+ "DT", "EM", "FIGCAPTION", "FIGURE", "FOOTER", "HEADER", "HGROUP", "I", "KBD", "MAIN", "MARK", "NAV", "NOSCRIPT", "RP",
142
+ "RT", "RUBY", "S", "SAMP", "SEARCH", "SECTION", "SMALL", "STRONG", "SUB", "SUMMARY", "SUP", "U", "VAR", "WBR",
143
+ "ACRONYM", "BASEFONT", "BIG", "CENTER", "NOBR", "NOEMBED", "NOFRAMES", "PLAINTEXT", "RB", "RTC", "STRIKE", "TT",
144
+ "FENCEDFRAME", "SELECTEDCONTENT", "GEOLOCATION", "PERMISSION"]);
145
+ /**
146
+ * Members the bindings define on every element that HTML gives only to
147
+ * some interfaces (HTML §4): each moves from Element.prototype to the
148
+ * prototypes of the interfaces listed, so `"play" in div` is false as in
149
+ * browsers. Interface names shared by several members are grouped.
150
+ */
151
+ const FORM_CONTROLS = ["HTMLInputElement", "HTMLSelectElement", "HTMLTextAreaElement", "HTMLButtonElement"];
152
+ const URL_ELEMENTS = ["HTMLAnchorElement", "HTMLAreaElement"];
153
+ const INTERFACE_MEMBERS = {
154
+ contentWindow: ["HTMLIFrameElement", "HTMLObjectElement", "HTMLFrameElement"],
155
+ contentDocument: ["HTMLIFrameElement", "HTMLObjectElement", "HTMLFrameElement"],
156
+ content: ["HTMLTemplateElement"],
157
+ paused: ["HTMLMediaElement"], ended: ["HTMLMediaElement"], currentTime: ["HTMLMediaElement"], duration: ["HTMLMediaElement"],
158
+ play: ["HTMLMediaElement"], pause: ["HTMLMediaElement"], load: ["HTMLMediaElement"], canPlayType: ["HTMLMediaElement"],
159
+ submit: ["HTMLFormElement"], requestSubmit: ["HTMLFormElement"], action: ["HTMLFormElement"], method: ["HTMLFormElement"],
160
+ elements: ["HTMLFormElement", "HTMLFieldSetElement"],
161
+ rows: ["HTMLTableElement", "HTMLTableSectionElement", "HTMLTextAreaElement"],
162
+ cells: ["HTMLTableRowElement"], rowIndex: ["HTMLTableRowElement"], cellIndex: ["HTMLTableCellElement"],
163
+ tBodies: ["HTMLTableElement"], tHead: ["HTMLTableElement"], tFoot: ["HTMLTableElement"],
164
+ options: ["HTMLSelectElement", "HTMLDataListElement"], selectedOptions: ["HTMLSelectElement"], selectedIndex: ["HTMLSelectElement"],
165
+ value: [...FORM_CONTROLS, "HTMLOptionElement", "HTMLOutputElement", "HTMLDataElement", "HTMLLIElement", "HTMLMeterElement",
166
+ "HTMLProgressElement", "HTMLParamElement"],
167
+ checked: ["HTMLInputElement"], selected: ["HTMLOptionElement"],
168
+ disabled: [...FORM_CONTROLS, "HTMLOptionElement", "HTMLOptGroupElement", "HTMLFieldSetElement", "HTMLLinkElement", "HTMLStyleElement"],
169
+ type: [...FORM_CONTROLS, "HTMLScriptElement", "HTMLLinkElement", "HTMLOListElement", "HTMLUListElement", "HTMLLIElement",
170
+ "HTMLEmbedElement", "HTMLObjectElement", "HTMLSourceElement", "HTMLOutputElement", "HTMLFieldSetElement", "HTMLParamElement",
171
+ ...URL_ELEMENTS],
172
+ name: [...FORM_CONTROLS, "HTMLFormElement", "HTMLIFrameElement", "HTMLObjectElement", "HTMLMapElement", "HTMLMetaElement",
173
+ "HTMLSlotElement", "HTMLOutputElement", "HTMLFieldSetElement", "HTMLParamElement", "HTMLAnchorElement", "HTMLImageElement",
174
+ "HTMLEmbedElement", "HTMLFrameElement", "HTMLDetailsElement"],
175
+ href: [...URL_ELEMENTS, "HTMLLinkElement", "HTMLBaseElement"],
176
+ src: ["HTMLImageElement", "HTMLScriptElement", "HTMLIFrameElement", "HTMLMediaElement", "HTMLSourceElement", "HTMLTrackElement",
177
+ "HTMLEmbedElement", "HTMLInputElement", "HTMLFrameElement"],
178
+ alt: ["HTMLImageElement", "HTMLAreaElement", "HTMLInputElement"],
179
+ placeholder: ["HTMLInputElement", "HTMLTextAreaElement"],
180
+ readOnly: ["HTMLInputElement", "HTMLTextAreaElement"],
181
+ required: ["HTMLInputElement", "HTMLTextAreaElement", "HTMLSelectElement"],
182
+ min: ["HTMLInputElement", "HTMLMeterElement"], max: ["HTMLInputElement", "HTMLMeterElement", "HTMLProgressElement"],
183
+ step: ["HTMLInputElement"],
184
+ rel: [...URL_ELEMENTS, "HTMLLinkElement"], target: [...URL_ELEMENTS, "HTMLFormElement", "HTMLBaseElement"],
185
+ htmlFor: ["HTMLLabelElement", "HTMLOutputElement"],
186
+ defaultValue: ["HTMLInputElement", "HTMLTextAreaElement", "HTMLOutputElement"],
187
+ origin: URL_ELEMENTS, protocol: URL_ELEMENTS, username: URL_ELEMENTS, password: URL_ELEMENTS, host: URL_ELEMENTS,
188
+ hostname: URL_ELEMENTS, port: URL_ELEMENTS, pathname: URL_ELEMENTS, search: URL_ELEMENTS, hash: URL_ELEMENTS,
189
+ };
59
190
  const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
191
+ const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
60
192
  const LIFECYCLE_EVENT_TYPES = new Set(["load", "DOMContentLoaded", "readystatechange"]);
193
+ // Classic scripts at least this long are prepared on the rewrite thread
194
+ // (see prepareOffThread); smaller ones cost less than the round trip.
195
+ const OFF_THREAD_SCRIPT_SIZE = 16 * 1024;
61
196
  // window.on* handler properties (window.onerror has a different signature and is not modelled).
62
197
  const WINDOW_HANDLER_EVENTS = [
63
198
  "load", "resize", "scroll", "keydown", "keyup", "keypress", "click", "message", "hashchange",
64
- "popstate", "beforeunload", "unload", "focus", "blur", "online", "offline",
199
+ "popstate", "beforeunload", "unload", "focus", "blur", "online", "offline", "unhandledrejection", "rejectionhandled",
65
200
  ];
66
201
  const hostStorage = new Map();
67
202
 
@@ -98,9 +233,11 @@ function cssEscape(value) {
98
233
  * viewport(): { width: number, height: number, scrollX: number, scrollY: number, devicePixelRatio: number },
99
234
  * scrollTo(x: number, y: number): void,
100
235
  * computedStyle(element: object): object | null,
236
+ * customProperties?(element: object): Map<string, string> | null,
101
237
  * navigate(url: string): void,
102
238
  * activateLink?(anchor: object): void,
103
239
  * fetchScript(url: string): Promise<string>,
240
+ * rewriteScript?: ((source: string) => Promise<{ code: string, parsed: boolean, strict: boolean }>) | null,
104
241
  * reportError(message: string): void,
105
242
  * consoleMessage?: ((level: string, text: string) => void) | null,
106
243
  * lifecycle?: (name: "DOMContentLoaded" | "load") => void,
@@ -126,19 +263,27 @@ export function createPageRealm({ window, document, mode, hooks }) {
126
263
  let nextTimerId = 1;
127
264
  let disposed = false;
128
265
  let readyState = "loading";
129
- // Navigation Timing 1 (performance.timing): epoch milliseconds on the same
130
- // timeline as performance.now(). Phases this document has not reached yet
131
- // read 0, as in browsers; the fetch phases all collapse onto the start.
132
- const timingStart = Math.round(hostPerformance.timeOrigin);
133
- const timingMarks = { domLoading: Math.round(hostPerformance.timeOrigin + hostPerformance.now()) };
266
+ // A document gets a fresh high-resolution time origin. Bun's
267
+ // performance.now() is relative to the renderer process, which is commonly
268
+ // pre-warmed several seconds before navigation. Page timestamps must use
269
+ // the document's timeline rather than include that pre-warm interval.
270
+ const documentStarted = hostPerformance.now();
271
+ const documentTimeOrigin = hostPerformance.timeOrigin + documentStarted;
272
+ const pageNow = () => hostPerformance.now() - documentStarted;
273
+ // Navigation Timing 1 (performance.timing): epoch milliseconds. Phases this
274
+ // document has not reached yet read 0; fetch phases collapse onto the start.
275
+ const timingStart = Math.round(documentTimeOrigin);
276
+ const timingMarks = { domLoading: Math.round(documentTimeOrigin) };
134
277
  const markTiming = (...names) => {
135
- const now = Math.round(hostPerformance.timeOrigin + hostPerformance.now());
278
+ const now = Math.round(documentTimeOrigin + pageNow());
136
279
  for (const name of names) timingMarks[name] = now;
137
280
  };
138
281
  let parsingHead = false;
139
282
  let currentScript = null;
140
283
  let pageGlobal = null;
141
284
  let evaluate;
285
+ // Runs a classic script already prepared off the main thread (SES mode only).
286
+ let evaluatePrepared = null;
142
287
  let evaluateModuleCode;
143
288
  let importMap = null;
144
289
  const modules = new Map();
@@ -154,11 +299,16 @@ export function createPageRealm({ window, document, mode, hooks }) {
154
299
  hooks.reportError(message);
155
300
  };
156
301
 
302
+ // The document's MediaSource registry. Assigned before any script runs;
303
+ // runTask publishes it so URL.createObjectURL reaches this document.
304
+ let mediaScope = null;
305
+
157
306
  /** Runs page code as a task: errors are reported, then the renderer may update. */
158
307
  const runTask = (task) => {
159
308
  if (disposed) return undefined;
309
+ if (mediaScope) blobUrlFallback = mediaScope;
160
310
  try {
161
- return task();
311
+ return blobUrlContext.run(mediaScope, () => asIncumbent(hooks.frames?.self, task));
162
312
  } catch (error) {
163
313
  reportError(error);
164
314
  return undefined;
@@ -181,12 +331,24 @@ export function createPageRealm({ window, document, mode, hooks }) {
181
331
  const realm = new BindingRealm(window, {
182
332
  touch: hooks.touch,
183
333
  boundsOf: hooks.boundsOf,
334
+ computedStyle: hooks.computedStyle,
335
+ // The engine clock's reading when this document's timeline started (see pageNow).
336
+ timeOriginOffset: () => documentStarted,
184
337
  viewport: hooks.viewport,
185
338
  scrollTo: hooks.scrollTo,
339
+ elementScroll: hooks.elementScroll,
340
+ elementAt: hooks.elementAt,
341
+ setElementScroll: hooks.setElementScroll,
342
+ scrollIntoView: hooks.scrollIntoView,
186
343
  mediaState: hooks.mediaState,
187
344
  playMedia: hooks.playMedia,
188
345
  pauseMedia: hooks.pauseMedia,
189
346
  seekMedia: hooks.seekMedia,
347
+ attachMediaSrc: (element, value) => hooks.attachMediaSrc?.(element, value),
348
+ mediaReadyState: (element) => hooks.mediaReadyState?.(element) ?? 0,
349
+ mediaNetworkState: (element) => hooks.mediaNetworkState?.(element) ?? 0,
350
+ mediaError: (element) => hooks.mediaError?.(element) ?? null,
351
+ mediaBuffered: (element) => hooks.mediaBuffered?.(element) ?? { length: 0, start: 0, end: 0 },
190
352
  call: callPage,
191
353
  readyState: () => readyState,
192
354
  body: () => parsingHead ? null : document.body,
@@ -201,28 +363,257 @@ export function createPageRealm({ window, document, mode, hooks }) {
201
363
  inserted: (nodes) => startInsertedScripts(nodes),
202
364
  activateLink: (anchor) => hooks.activateLink?.(anchor),
203
365
  submitForm: (form, submitter) => hooks.submitForm?.(form, submitter),
366
+ contentWindow: (node) => messaging.contentWindow(node),
367
+ contentDocument: (node) => messaging.contentDocument(node),
204
368
  acceptEvent: (event) => !LIFECYCLE_EVENT_TYPES.has(event.type)
205
369
  || (event.target !== window && event.target !== document)
206
370
  || lifecycleEvents.has(event),
207
371
  });
208
372
  const eventTargets = eventTargetConstructor(realm,
209
- (value) => value === pageGlobal || Boolean(nodeOf(value)));
373
+ (value) => value === pageGlobal || Boolean(nodeOf(value)),
374
+ (value) => value === pageGlobal ? window : nodeOf(value));
210
375
  const documentBinding = realm.wrap(document);
211
376
  const NodeInterface = function Node() { throw new TypeError("Illegal constructor"); };
212
- NodeInterface.prototype = mutableBindingPrototype(INTERFACES.Node.prototype);
377
+ // Node.prototype inherits EventTarget.prototype (DOM §4.4), as Window.prototype does.
378
+ NodeInterface.prototype = mutableBindingPrototype(INTERFACES.Node.prototype, eventTargets.EventTarget.prototype);
379
+ const WindowInterface = function Window() { throw new TypeError("Illegal constructor"); };
380
+ WindowInterface.prototype = Object.create(eventTargets.EventTarget.prototype);
381
+ Object.defineProperty(WindowInterface.prototype, "constructor", { value: WindowInterface, writable: true, configurable: true });
382
+ Object.defineProperty(WindowInterface, Symbol.hasInstance, { value: (value) => value === pageGlobal });
213
383
  Object.defineProperty(NodeInterface.prototype, "constructor", { value: NodeInterface, writable: true, configurable: true });
384
+ // Node.ELEMENT_NODE, Node.DOCUMENT_POSITION_FOLLOWING, ...
385
+ for (const name of Object.keys(INTERFACES.Node)) {
386
+ if (/^[A-Z_]+$/.test(name)) Object.defineProperty(NodeInterface, name, { value: INTERFACES.Node[name], enumerable: true });
387
+ }
214
388
  realm.nodePrototype = NodeInterface.prototype;
215
389
  const parentPrototype = mutableBindingPrototype(Object.getPrototypeOf(INTERFACES.Element.prototype), NodeInterface.prototype);
390
+ // The ParentNode mixin's members (children, querySelector, append, ...) are
391
+ // own properties of Element.prototype, Document.prototype and
392
+ // DocumentFragment.prototype, as WebIDL places them: ShadyDOM saves the
393
+ // native accessors with Object.getOwnPropertyDescriptor on each of them.
394
+ const withParentNodeMembers = (prototype) => {
395
+ for (const key of Reflect.ownKeys(parentPrototype)) {
396
+ if (key !== "constructor" && !Object.hasOwn(prototype, key)) {
397
+ Object.defineProperty(prototype, key, Object.getOwnPropertyDescriptor(parentPrototype, key));
398
+ }
399
+ }
400
+ return prototype;
401
+ };
216
402
  const ElementInterface = function Element() { throw new TypeError("Illegal constructor"); };
217
- ElementInterface.prototype = mutableBindingPrototype(INTERFACES.Element.prototype, parentPrototype);
403
+ ElementInterface.prototype = withParentNodeMembers(mutableBindingPrototype(INTERFACES.Element.prototype, parentPrototype));
218
404
  Object.defineProperty(ElementInterface.prototype, "constructor", { value: ElementInterface, writable: true, configurable: true });
219
405
  const HTMLElementInterface = function HTMLElement() {
220
406
  if (!new.target || !constructingElement) throw new TypeError("Illegal constructor");
221
407
  return realm.wrap(constructingElement);
222
408
  };
223
- HTMLElementInterface.prototype = mutableBindingPrototype(ElementInterface.prototype);
409
+ // HTMLElement.prototype inherits Element's members rather than copying
410
+ // them, so what a page patches on Element.prototype (custom element
411
+ // polyfills wrap setAttribute, appendChild...) reaches HTML elements; the
412
+ // members HTMLElement itself defines move there.
413
+ HTMLElementInterface.prototype = Object.create(ElementInterface.prototype);
414
+ for (const key of Reflect.ownKeys(ElementInterface.prototype)) {
415
+ if (typeof key !== "string" || !(HTML_ELEMENT_MEMBERS.has(key) || key.startsWith("on"))) continue;
416
+ Object.defineProperty(HTMLElementInterface.prototype, key, Object.getOwnPropertyDescriptor(ElementInterface.prototype, key));
417
+ delete ElementInterface.prototype[key];
418
+ }
224
419
  Object.defineProperty(HTMLElementInterface.prototype, "constructor", { value: HTMLElementInterface, writable: true, configurable: true });
225
420
  realm.elementPrototype = HTMLElementInterface.prototype;
421
+ // HTMLDivElement.prototype, HTMLSlotElement.prototype, ...: each HTML
422
+ // element's wrapper inherits its interface's prototype, so what a page adds
423
+ // there (ShadyDOM patches HTMLSlotElement.prototype) reaches its elements.
424
+ const elementInterfacePrototypes = new Map();
425
+ const tagPrototypes = new Map();
426
+ const mediaPrototype = Object.create(HTMLElementInterface.prototype);
427
+ elementInterfacePrototypes.set("HTMLMediaElement", mediaPrototype);
428
+ for (const [name, tags] of Object.entries(ELEMENT_INTERFACES)) {
429
+ if (name === "HTMLMediaElement") continue;
430
+ const prototype = Object.create(tags.every((tag) => ELEMENT_INTERFACES.HTMLMediaElement.includes(tag))
431
+ ? mediaPrototype : HTMLElementInterface.prototype);
432
+ elementInterfacePrototypes.set(name, prototype);
433
+ for (const tag of tags) tagPrototypes.set(tag, prototype);
434
+ }
435
+ for (const [key, names] of Object.entries(INTERFACE_MEMBERS)) {
436
+ const descriptor = Object.getOwnPropertyDescriptor(ElementInterface.prototype, key);
437
+ if (!descriptor) continue;
438
+ for (const name of names) {
439
+ const prototype = elementInterfacePrototypes.get(name);
440
+ // HTMLMediaElement's members reach audio and video through it.
441
+ if (prototype && !Object.hasOwn(prototype, key)) Object.defineProperty(prototype, key, descriptor);
442
+ }
443
+ delete ElementInterface.prototype[key];
444
+ }
445
+ const unknownPrototype = Object.create(HTMLElementInterface.prototype);
446
+ elementInterfacePrototypes.set("HTMLUnknownElement", unknownPrototype);
447
+ const isUnknownElement = (node) => node.namespaceURI === HTML_NAMESPACE && !tagPrototypes.has(node.tagName)
448
+ && !PLAIN_HTML_ELEMENTS.has(node.tagName) && !node.localName.includes("-");
449
+ realm.elementPrototypeOf = (node) => {
450
+ if (node.namespaceURI !== HTML_NAMESPACE) return HTMLElementInterface.prototype;
451
+ return tagPrototypes.get(node.tagName) ?? (isUnknownElement(node) ? unknownPrototype : HTMLElementInterface.prototype);
452
+ };
453
+ for (const [name, attributes] of Object.entries(REFLECTED_ATTRIBUTES)) {
454
+ const prototype = elementInterfacePrototypes.get(name);
455
+ for (const [key, [attribute, type]] of Object.entries(attributes)) {
456
+ const read = (element) => {
457
+ const value = nodeOf(element).getAttribute(attribute);
458
+ if (type === "string") return attribute === "fetchpriority" ? (["high", "low"].includes(value) ? value : "auto") : value ?? "";
459
+ const number = /^\s*\d+/.exec(value ?? "") ? Number.parseInt(value, 10) : 0;
460
+ return number >= 0 && number <= 2147483647 ? number : 0;
461
+ };
462
+ Object.defineProperty(prototype, key, {
463
+ configurable: true,
464
+ enumerable: true,
465
+ get: Object.getOwnPropertyDescriptor({ get [key]() { return read(this); } }, key).get,
466
+ set: Object.getOwnPropertyDescriptor({ set [key](value) {
467
+ nodeOf(this).setAttribute(attribute, String(type === "unsigned" ? Number(value) >>> 0 : value));
468
+ realm.hooks.touch(nodeOf(this));
469
+ } }, key).set,
470
+ });
471
+ }
472
+ }
473
+ // Interface-specific attributes Happy DOM implements (HTMLImageElement's
474
+ // complete, naturalWidth, useMap, decoding...): each interface prototype
475
+ // gets an accessor reading and writing the node. Only attributes whose
476
+ // values are primitives are exposed this way; an engine object never
477
+ // reaches the page through them.
478
+ const interfaceOrder = Object.entries(ELEMENT_INTERFACES).sort(([a], [b]) => (b === "HTMLMediaElement") - (a === "HTMLMediaElement"));
479
+ for (const [name, tags] of interfaceOrder) {
480
+ const prototype = elementInterfacePrototypes.get(name);
481
+ const EngineInterface = window[name];
482
+ if (!prototype || typeof EngineInterface !== "function" || !tags.length) continue;
483
+ let sample;
484
+ let sampleWrapper;
485
+ try {
486
+ sample = document.createElement(tags[0].toLowerCase());
487
+ sampleWrapper = Object.setPrototypeOf(realm.wrap(sample), prototype);
488
+ } catch {
489
+ continue;
490
+ }
491
+ const stop = window.HTMLElement?.prototype;
492
+ for (let engine = EngineInterface.prototype; engine && engine !== stop; engine = Object.getPrototypeOf(engine)) {
493
+ for (const key of Object.getOwnPropertyNames(engine)) {
494
+ if (key === "constructor" || /^on/.test(key) || Object.hasOwn(prototype, key)) continue;
495
+ const descriptor = Object.getOwnPropertyDescriptor(engine, key);
496
+ if (!descriptor?.get) continue;
497
+ let value;
498
+ try {
499
+ value = descriptor.get.call(sample);
500
+ } catch {
501
+ continue;
502
+ }
503
+ // null is only a value of nullable string attributes; elsewhere it stands for an object.
504
+ if (value === null ? !NULLABLE_STRING_ATTRIBUTES.has(key) : !["string", "number", "boolean"].includes(typeof value)) continue;
505
+ // The bindings' own members (contentWindow, value, ...) stay, unless
506
+ // they are another element's and read undefined here (img.name).
507
+ if (key in prototype) {
508
+ let own;
509
+ try {
510
+ own = Reflect.get(prototype, key, sampleWrapper);
511
+ } catch {
512
+ continue;
513
+ }
514
+ if (own !== undefined) continue;
515
+ }
516
+ const { get, set } = descriptor;
517
+ Object.defineProperty(prototype, key, {
518
+ configurable: true,
519
+ enumerable: true,
520
+ get: Object.getOwnPropertyDescriptor({ get [key]() {
521
+ const result = get.call(nodeOf(this));
522
+ return result === null || ["string", "number", "boolean"].includes(typeof result) ? result : null;
523
+ } }, key).get,
524
+ set: set ? Object.getOwnPropertyDescriptor({ set [key](next) {
525
+ const node = nodeOf(this);
526
+ set.call(node, next === null || typeof next !== "object" ? next : String(next));
527
+ realm.hooks.touch(node);
528
+ } }, key).set : undefined,
529
+ });
530
+ }
531
+ }
532
+ }
533
+ // HTMLCanvasElement width/height are the bitmap size (default 300×150), and
534
+ // getContext("2d") is how pages sample colours and draw. Other elements do
535
+ // not inherit these.
536
+ const canvasPrototype = elementInterfacePrototypes.get("HTMLCanvasElement");
537
+ for (const name of ["width", "height"]) {
538
+ Object.defineProperty(canvasPrototype, name, {
539
+ configurable: true,
540
+ enumerable: true,
541
+ get() {
542
+ return nodeOf(this)[name];
543
+ },
544
+ set(value) {
545
+ const node = nodeOf(this);
546
+ node[name] = value >>> 0;
547
+ resetCanvas(node);
548
+ realm.hooks.touch(node);
549
+ },
550
+ });
551
+ }
552
+ Object.defineProperty(canvasPrototype, "getContext", {
553
+ configurable: true,
554
+ enumerable: true,
555
+ writable: true,
556
+ value(type) {
557
+ return canvasContext(nodeOf(this), String(type));
558
+ },
559
+ });
560
+ // HTMLMediaElement. HAVE_* / NETWORK_* / MEDIA_ERR_* live on the prototype
561
+ // and on the interface object (feature checks read either place).
562
+ const MEDIA_CONSTANTS = {
563
+ HAVE_NOTHING: 0, HAVE_METADATA: 1, HAVE_CURRENT_DATA: 2, HAVE_FUTURE_DATA: 3, HAVE_ENOUGH_DATA: 4,
564
+ NETWORK_EMPTY: 0, NETWORK_IDLE: 1, NETWORK_LOADING: 2, NETWORK_NO_SOURCE: 3,
565
+ MEDIA_ERR_ABORTED: 1, MEDIA_ERR_NETWORK: 2, MEDIA_ERR_DECODE: 3, MEDIA_ERR_SRC_NOT_SUPPORTED: 4,
566
+ };
567
+ for (const [name, value] of Object.entries(MEDIA_CONSTANTS)) {
568
+ Object.defineProperty(mediaPrototype, name, { value, enumerable: true });
569
+ }
570
+ const mediaSrcObjects = new WeakMap();
571
+ const mediaTimeRanges = (range) => ({
572
+ length: range.length,
573
+ start(index) {
574
+ if (index !== 0 || !range.length) throw new DOMException("The index provided is out of range.", "IndexSizeError");
575
+ return range.start;
576
+ },
577
+ end(index) {
578
+ if (index !== 0 || !range.length) throw new DOMException("The index provided is out of range.", "IndexSizeError");
579
+ return range.end;
580
+ },
581
+ });
582
+ for (const [name, get] of Object.entries({
583
+ readyState: (node) => realm.hooks.mediaReadyState?.(node) ?? 0,
584
+ networkState: (node) => realm.hooks.mediaNetworkState?.(node) ?? 0,
585
+ error: (node) => realm.hooks.mediaError?.(node) ?? null,
586
+ buffered: (node) => mediaTimeRanges(realm.hooks.mediaBuffered?.(node) ?? { length: 0, start: 0, end: 0 }),
587
+ })) {
588
+ Object.defineProperty(mediaPrototype, name, {
589
+ configurable: true,
590
+ enumerable: true,
591
+ get() {
592
+ return get(nodeOf(this));
593
+ },
594
+ });
595
+ }
596
+ const videoPrototype = elementInterfacePrototypes.get("HTMLVideoElement");
597
+ for (const name of ["videoWidth", "videoHeight"]) {
598
+ Object.defineProperty(videoPrototype, name, {
599
+ configurable: true,
600
+ enumerable: true,
601
+ get() {
602
+ return realm.hooks.mediaState?.(nodeOf(this))?.[name] ?? 0;
603
+ },
604
+ });
605
+ }
606
+ Object.defineProperty(mediaPrototype, "srcObject", {
607
+ configurable: true,
608
+ enumerable: true,
609
+ get() {
610
+ return mediaSrcObjects.get(this) ?? null;
611
+ },
612
+ set(value) {
613
+ mediaSrcObjects.set(this, value);
614
+ realm.hooks.attachMediaSrc?.(nodeOf(this), value);
615
+ },
616
+ });
226
617
  const TextInterface = function Text() { throw new TypeError("Illegal constructor"); };
227
618
  const CharacterDataInterface = function CharacterData() { throw new TypeError("Illegal constructor"); };
228
619
  CharacterDataInterface.prototype = mutableBindingPrototype(INTERFACES.CharacterData.prototype, NodeInterface.prototype);
@@ -232,15 +623,36 @@ export function createPageRealm({ window, document, mode, hooks }) {
232
623
  Object.defineProperty(TextInterface.prototype, "constructor", { value: TextInterface, writable: true, configurable: true });
233
624
  realm.textPrototype = TextInterface.prototype;
234
625
  const DocumentFragmentInterface = function DocumentFragment() { throw new TypeError("Illegal constructor"); };
235
- DocumentFragmentInterface.prototype = mutableBindingPrototype(INTERFACES.DocumentFragment.prototype, parentPrototype);
626
+ DocumentFragmentInterface.prototype = withParentNodeMembers(mutableBindingPrototype(INTERFACES.DocumentFragment.prototype, parentPrototype));
236
627
  Object.defineProperty(DocumentFragmentInterface.prototype, "constructor", { value: DocumentFragmentInterface, writable: true, configurable: true });
237
628
  realm.fragmentPrototype = DocumentFragmentInterface.prototype;
238
629
  realm.shadowRootPrototype = mutableBindingPrototype(INTERFACES.ShadowRoot.prototype, DocumentFragmentInterface.prototype);
239
630
  const DocumentInterface = function Document() { throw new TypeError("Illegal constructor"); };
240
- DocumentInterface.prototype = mutableBindingPrototype(INTERFACES.Document.prototype, parentPrototype);
631
+ DocumentInterface.prototype = withParentNodeMembers(mutableBindingPrototype(INTERFACES.Document.prototype, parentPrototype));
241
632
  Object.defineProperty(DocumentInterface.prototype, "constructor", { value: DocumentInterface, writable: true, configurable: true });
242
633
  Object.setPrototypeOf(documentBinding, DocumentInterface.prototype);
243
634
 
635
+ /** In an iframe's document, parent and top are the embedding frames' windows. */
636
+ const defineFrameWindows = (global) => {
637
+ const parent = messaging.parent;
638
+ const top = messaging.top;
639
+ if (parent) Object.defineProperty(global, "parent", { value: parent, writable: true, configurable: true });
640
+ if (top) Object.defineProperty(global, "top", { value: top, writable: true, configurable: true });
641
+ Object.defineProperty(global, "frames", { value: messaging.framesOf(global), writable: true, configurable: true });
642
+ // window.name is the frame's (browsing context's) name.
643
+ const self = hooks.frames?.self;
644
+ let ownName = "";
645
+ Object.defineProperty(global, "name", {
646
+ get: () => self ? self.name() : ownName,
647
+ set: (value) => {
648
+ if (self) self.setName(value);
649
+ else ownName = String(value);
650
+ },
651
+ enumerable: true,
652
+ configurable: true,
653
+ });
654
+ };
655
+
244
656
  const fireLifecycle = (target, event) => {
245
657
  lifecycleEvents.add(event);
246
658
  target.dispatchEvent(event);
@@ -275,13 +687,33 @@ export function createPageRealm({ window, document, mode, hooks }) {
275
687
  hooks.consoleMessage?.(level, text);
276
688
  },
277
689
  ]));
278
- pageConsole.table = pageConsole.log;
279
- pageConsole.dir = pageConsole.log;
280
- pageConsole.group = pageConsole.log;
281
- pageConsole.groupEnd = () => {};
282
- pageConsole.assert = (condition, ...args) => {
690
+ // Each console method is its own function, as in browsers (and each prints
691
+ // its own name). Grouping, timing and counting log a line or nothing.
692
+ for (const name of ["table", "dir", "dirxml", "group", "groupCollapsed"]) {
693
+ pageConsole[name] = (...args) => pageConsole.log(...args);
694
+ }
695
+ for (const name of ["groupEnd", "clear", "profile", "profileEnd", "timeStamp"]) pageConsole[name] = () => {};
696
+ const consoleCounts = new Map();
697
+ const consoleTimers = new Map();
698
+ pageConsole.count = (label = "default") => {
699
+ const count = (consoleCounts.get(String(label)) ?? 0) + 1;
700
+ consoleCounts.set(String(label), count);
701
+ pageConsole.info(`${label}: ${count}`);
702
+ };
703
+ pageConsole.countReset = (label = "default") => { consoleCounts.delete(String(label)); };
704
+ pageConsole.time = (label = "default") => { consoleTimers.set(String(label), hostPerformance.now()); };
705
+ pageConsole.timeLog = (label = "default", ...args) => {
706
+ const start = consoleTimers.get(String(label));
707
+ if (start !== undefined) pageConsole.info(`${label}: ${hostPerformance.now() - start} ms`, ...args);
708
+ };
709
+ pageConsole.timeEnd = (label = "default") => {
710
+ pageConsole.timeLog(label);
711
+ consoleTimers.delete(String(label));
712
+ };
713
+ pageConsole.assert = (condition = undefined, ...args) => {
283
714
  if (!condition) pageConsole.error("Assertion failed:", ...args);
284
715
  };
716
+ Object.defineProperty(pageConsole, Symbol.toStringTag, { value: "console", configurable: true });
285
717
 
286
718
  /**
287
719
  * MutationObserver wraps Happy DOM's implementation (records carry node
@@ -357,7 +789,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
357
789
  const visible = rect.top < height && rect.top + rect.height > 0 && rect.left < width && rect.left + rect.width > 0;
358
790
  return {
359
791
  target,
360
- time: hostPerformance.now(),
792
+ time: pageNow(),
361
793
  isIntersecting: visible,
362
794
  intersectionRatio: visible ? 1 : 0,
363
795
  boundingClientRect: rectangle(rect),
@@ -374,17 +806,166 @@ export function createPageRealm({ window, document, mode, hooks }) {
374
806
  * URLs are read locally. Bodies are normalised with Bun's Response, which
375
807
  * also yields the right Content-Type (multipart boundaries included).
376
808
  */
809
+ /**
810
+ * performance (High Resolution Time, User Timing 3): marks and measures the
811
+ * page records, and PerformanceObserver for them. Other entry types
812
+ * (resource, paint, long tasks) are not recorded; observers of them stay quiet.
813
+ */
814
+ function userTimingApi() {
815
+ const entries = [];
816
+ const observers = new Set();
817
+ const SUPPORTED = Object.freeze(["mark", "measure"]);
818
+ const entry = (entryType, name, startTime, duration, detail) => Object.freeze({
819
+ entryType, name, startTime, duration, detail: detail ?? null,
820
+ toJSON() { return { entryType, name, startTime, duration, detail: detail ?? null }; },
821
+ });
822
+ // Observers get their entries in a task of their own, batched.
823
+ const schedule = (observer) => {
824
+ if (observer.scheduled) return;
825
+ observer.scheduled = true;
826
+ hostSetTimeout(() => runTask(() => {
827
+ observer.scheduled = false;
828
+ const list = observer.buffer.splice(0);
829
+ if (!list.length || !observers.has(observer)) return;
830
+ callPage(observer.callback, observer.wrapper, [entryList(list), observer.wrapper]);
831
+ }), 0);
832
+ };
833
+ const notify = (recorded) => {
834
+ for (const observer of observers) {
835
+ if (!observer.types.has(recorded.entryType)) continue;
836
+ observer.buffer.push(recorded);
837
+ schedule(observer);
838
+ }
839
+ };
840
+ const record = (recorded) => {
841
+ entries.push(recorded);
842
+ if (entries.length > 10_000) entries.shift();
843
+ notify(recorded);
844
+ return recorded;
845
+ };
846
+ const markTime = (value, fallback) => {
847
+ if (value === undefined || value === null) return fallback;
848
+ if (typeof value === "number") return value;
849
+ const found = entries.findLast((candidate) => candidate.entryType === "mark" && candidate.name === String(value));
850
+ if (!found) throw new DOMException(`The mark '${value}' does not exist.`, "SyntaxError");
851
+ return found.startTime;
852
+ };
853
+ const entryList = (list) => Object.freeze({
854
+ getEntries: () => [...list],
855
+ getEntriesByType: (type) => list.filter((candidate) => candidate.entryType === String(type)),
856
+ getEntriesByName: (name, type = undefined) => list.filter((candidate) => candidate.name === String(name)
857
+ && (type === undefined || candidate.entryType === String(type))),
858
+ });
859
+ const performance = {
860
+ now: pageNow,
861
+ timeOrigin: documentTimeOrigin,
862
+ timing: navigationTiming(timingStart, timingMarks),
863
+ navigation: Object.freeze({ type: 0, redirectCount: 0, TYPE_NAVIGATE: 0, TYPE_RELOAD: 1, TYPE_BACK_FORWARD: 2, TYPE_RESERVED: 255 }),
864
+ mark(name, options = undefined) {
865
+ return record(entry("mark", String(name), options?.startTime ?? pageNow(), 0, options?.detail));
866
+ },
867
+ measure(name, start = undefined, end = undefined) {
868
+ const options = typeof start === "object" && start !== null ? start : null;
869
+ const now = pageNow();
870
+ const startTime = markTime(options ? options.start : start, 0);
871
+ const endTime = options?.duration !== undefined && options.end === undefined
872
+ ? startTime + Number(options.duration)
873
+ : markTime(options ? options.end : end, now);
874
+ return record(entry("measure", String(name), startTime, endTime - startTime, options?.detail));
875
+ },
876
+ clearMarks(name = undefined) {
877
+ for (let index = entries.length - 1; index >= 0; index--) {
878
+ if (entries[index].entryType === "mark" && (name === undefined || entries[index].name === String(name))) entries.splice(index, 1);
879
+ }
880
+ },
881
+ clearMeasures(name = undefined) {
882
+ for (let index = entries.length - 1; index >= 0; index--) {
883
+ if (entries[index].entryType === "measure" && (name === undefined || entries[index].name === String(name))) entries.splice(index, 1);
884
+ }
885
+ },
886
+ clearResourceTimings() {},
887
+ setResourceTimingBufferSize(maxSize) {},
888
+ getEntries: () => [...entries],
889
+ getEntriesByType: (type) => entries.filter((candidate) => candidate.entryType === String(type)),
890
+ getEntriesByName: (name, type = undefined) => entries.filter((candidate) => candidate.name === String(name)
891
+ && (type === undefined || candidate.entryType === String(type))),
892
+ toJSON: () => ({ timeOrigin: documentTimeOrigin }),
893
+ };
894
+ function PerformanceObserver(callback) {
895
+ if (!new.target) throw new TypeError("Constructor PerformanceObserver requires 'new'");
896
+ if (typeof callback !== "function") throw new TypeError("PerformanceObserver: callback is not a function");
897
+ const wrapper = this;
898
+ const state = { callback, wrapper, types: new Set(), buffer: [], scheduled: false };
899
+ Object.defineProperties(this, {
900
+ observe: {
901
+ value(options = {}) {
902
+ const types = options.type !== undefined ? [String(options.type)] : [...(options.entryTypes ?? [])].map(String);
903
+ for (const type of types) state.types.add(type);
904
+ observers.add(state);
905
+ if (options.buffered && options.type !== undefined) {
906
+ for (const existing of entries) if (state.types.has(existing.entryType)) state.buffer.push(existing);
907
+ if (state.buffer.length) schedule(state);
908
+ }
909
+ },
910
+ },
911
+ disconnect: { value() { observers.delete(state); state.buffer.length = 0; } },
912
+ takeRecords: { value: () => state.buffer.splice(0) },
913
+ });
914
+ }
915
+ Object.defineProperty(PerformanceObserver, "supportedEntryTypes", { value: SUPPORTED });
916
+ return { performance, PerformanceObserver };
917
+ }
918
+
377
919
  function networkApi() {
378
920
  const abortError = () => new DOMException("The operation was aborted.", "AbortError");
921
+ const baseUrl = () => document.baseURI || document.URL;
922
+
923
+ // new Request("/api") resolves against the document's base URL (Fetch
924
+ // §5.4), which the host's Request has no document to do.
925
+ const PageRequest = class Request extends globalThis.Request {
926
+ constructor(input, init = undefined) {
927
+ super(input instanceof globalThis.Request ? input : new URL(String(input), baseUrl()).href, init);
928
+ // Fetch §5.4 step 36: a string body gives the request its Content-Type
929
+ // (Bun's Request only does this for the other body kinds).
930
+ if (typeof init?.body === "string" && !this.headers.has("content-type")) {
931
+ this.headers.set("content-type", "text/plain;charset=UTF-8");
932
+ }
933
+ }
934
+ };
935
+
936
+ // fetch(request) reads the Request's internal state, as Chromium's
937
+ // Request::CreateRequestWithRequestOrString does with GetRequest(). Page
938
+ // script may shadow url, method or bodyUsed on the instance; that changes
939
+ // what script reads, not what is fetched.
940
+ const requestSlot = (name) => {
941
+ const get = Object.getOwnPropertyDescriptor(globalThis.Request.prototype, name).get;
942
+ return (request) => get.call(request);
943
+ };
944
+ const slots = Object.fromEntries(["url", "method", "headers", "mode", "credentials", "redirect", "signal", "bodyUsed"]
945
+ .map((name) => [name, requestSlot(name)]));
946
+ const cloneRequest = globalThis.Request.prototype.clone;
947
+ /** The input when it is a platform Request (a brand check, not instanceof). */
948
+ const platformRequest = (input) => {
949
+ if (input === null || typeof input !== "object") return null;
950
+ try {
951
+ slots.url(input);
952
+ return input;
953
+ } catch {
954
+ return null;
955
+ }
956
+ };
379
957
 
380
958
  async function describeRequest(input, init = {}) {
381
- const source = input instanceof Request ? input : null;
382
- const url = new URL(source ? source.url : String(input), document.URL);
383
- const method = String(init.method ?? source?.method ?? "GET").toUpperCase();
384
- const headers = new Headers(init.headers ?? source?.headers ?? undefined);
959
+ const source = platformRequest(input);
960
+ if (source && init.body === undefined && slots.bodyUsed(source)) {
961
+ throw new TypeError("Cannot construct a Request with a Request object that has already been used.");
962
+ }
963
+ const url = new URL(source ? slots.url(source) : String(input), baseUrl());
964
+ const method = String(init.method ?? (source ? slots.method(source) : "GET")).toUpperCase();
965
+ const headers = new Headers(init.headers ?? (source ? slots.headers(source) : undefined));
385
966
  let body = null;
386
967
  const bodyInit = init.body !== undefined ? init.body : source && !["GET", "HEAD"].includes(method)
387
- ? await source.clone().arrayBuffer() : null;
968
+ ? await cloneRequest.call(source).arrayBuffer() : null;
388
969
  if (bodyInit !== null && bodyInit !== undefined) {
389
970
  if (["GET", "HEAD"].includes(method)) throw new TypeError("Request with GET/HEAD method cannot have body.");
390
971
  const normalized = new Response(bodyInit);
@@ -400,22 +981,18 @@ export function createPageRealm({ window, document, mode, hooks }) {
400
981
  method,
401
982
  headers: [...headers],
402
983
  body,
403
- mode: init.mode ?? source?.mode ?? "cors",
404
- credentials: init.credentials ?? source?.credentials ?? "same-origin",
405
- redirect: init.redirect ?? source?.redirect ?? "follow",
406
- signal: init.signal ?? source?.signal ?? null,
984
+ mode: init.mode ?? (source ? slots.mode(source) : "cors"),
985
+ credentials: init.credentials ?? (source ? slots.credentials(source) : "same-origin"),
986
+ redirect: init.redirect ?? (source ? slots.redirect(source) : "follow"),
987
+ signal: init.signal ?? (source ? slots.signal(source) : null),
407
988
  };
408
989
  }
409
990
 
410
991
  function makeResponse(data) {
411
- const bytes = data.body instanceof Uint8Array ? data.body : new Uint8Array(0);
412
992
  const headers = new Headers(data.headers ?? []);
413
- let used = false;
414
- const read = () => {
415
- if (used) return Promise.reject(new TypeError("Body has already been used"));
416
- used = true;
417
- return Promise.resolve(new Response(bytes, { headers: { "content-type": headers.get("content-type") ?? "" } }));
418
- };
993
+ const body = data.body instanceof ReadableStream ? data.body
994
+ : data.body instanceof Uint8Array ? data.body : new Uint8Array(0);
995
+ const native = new Response(body, { headers: { "content-type": headers.get("content-type") ?? "" } });
419
996
  const response = {
420
997
  type: data.type ?? "basic",
421
998
  url: data.url ?? "",
@@ -425,20 +1002,20 @@ export function createPageRealm({ window, document, mode, hooks }) {
425
1002
  statusText: data.statusText ?? "",
426
1003
  headers,
427
1004
  get bodyUsed() {
428
- return used;
1005
+ return native.bodyUsed;
429
1006
  },
430
1007
  get body() {
431
- return used ? null : new Response(bytes).body;
1008
+ return native.body;
432
1009
  },
433
- text: () => read().then((body) => body.text()),
434
- json: () => read().then((body) => body.json()),
435
- arrayBuffer: () => read().then((body) => body.arrayBuffer()),
436
- blob: () => read().then((body) => body.blob()),
437
- formData: () => read().then((body) => body.formData()),
438
- bytes: () => read().then((body) => body.bytes()),
1010
+ text: () => native.text(),
1011
+ json: () => native.json(),
1012
+ arrayBuffer: () => native.arrayBuffer(),
1013
+ blob: () => native.blob(),
1014
+ formData: () => native.formData(),
1015
+ bytes: () => native.bytes(),
439
1016
  clone: () => {
440
- if (used) throw new TypeError("Body has already been used");
441
- return makeResponse(data);
1017
+ const copy = native.clone();
1018
+ return makeResponse({ ...data, body: copy.body });
442
1019
  },
443
1020
  };
444
1021
  return response;
@@ -450,16 +1027,29 @@ export function createPageRealm({ window, document, mode, hooks }) {
450
1027
  if (signal?.aborted) throw abortError();
451
1028
  const scheme = new URL(request.url).protocol;
452
1029
  let pending;
1030
+ let networkPending = null;
453
1031
  if (scheme === "data:" || scheme === "blob:") {
454
- pending = fetch(request.url).then(async (local) => ({
455
- type: "basic", url: request.url, status: local.status, statusText: local.statusText,
456
- headers: [...local.headers], body: new Uint8Array(await local.arrayBuffer()),
457
- }));
1032
+ const stored = scheme === "blob:" ? mediaScope?.lookup(request.url) : null;
1033
+ if (stored?.kind === "media-source") {
1034
+ pending = Promise.reject(new TypeError("Failed to fetch"));
1035
+ } else if (stored?.kind === "blob") {
1036
+ pending = stored.blob.arrayBuffer().then((buffer) => ({
1037
+ type: "basic", url: request.url, status: 200, statusText: "OK",
1038
+ headers: [["content-type", stored.blob.type || ""]],
1039
+ body: new Uint8Array(buffer),
1040
+ }));
1041
+ } else {
1042
+ pending = fetch(request.url).then(async (local) => ({
1043
+ type: "basic", url: request.url, status: local.status, statusText: local.statusText,
1044
+ headers: [...local.headers], body: new Uint8Array(await local.arrayBuffer()),
1045
+ }));
1046
+ }
458
1047
  } else {
459
1048
  const { signal: _signal, ...plain } = request;
1049
+ networkPending = hooks.pageFetch(plain);
460
1050
  // Network and CORS failures reach the page as a bare TypeError, as in
461
1051
  // browsers; the reason is only logged to the console.
462
- pending = hooks.pageFetch(plain).catch((error) => {
1052
+ pending = networkPending.catch((error) => {
463
1053
  const text = `fetch ${plain.url}: ${error?.message ?? error}`;
464
1054
  consoleEntries.push({ level: "error", text });
465
1055
  hooks.consoleMessage?.("error", text);
@@ -467,7 +1057,10 @@ export function createPageRealm({ window, document, mode, hooks }) {
467
1057
  });
468
1058
  }
469
1059
  const aborted = signal
470
- ? new Promise((_, reject) => signal.addEventListener("abort", () => reject(abortError()), { once: true }))
1060
+ ? new Promise((_, reject) => signal.addEventListener("abort", () => {
1061
+ networkPending?.abort?.();
1062
+ reject(abortError());
1063
+ }, { once: true }))
471
1064
  : null;
472
1065
  const data = await (aborted ? Promise.race([pending, aborted]) : pending);
473
1066
  hooks.afterTask();
@@ -501,7 +1094,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
501
1094
  throw new DOMException("Synchronous XMLHttpRequest is not supported", "InvalidAccessError");
502
1095
  }
503
1096
  method = String(requestMethod).toUpperCase();
504
- url = new URL(String(requestUrl), document.URL).href;
1097
+ url = new URL(String(requestUrl), baseUrl()).href;
505
1098
  setState(1);
506
1099
  },
507
1100
  setRequestHeader(name, value) {
@@ -591,7 +1184,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
591
1184
  if (!new.target) throw new TypeError("Constructor WebSocket requires 'new'");
592
1185
  let target;
593
1186
  try {
594
- target = new URL(String(url), document.URL);
1187
+ target = new URL(String(url), baseUrl());
595
1188
  } catch {
596
1189
  throw new DOMException(`Failed to construct 'WebSocket': The URL '${url}' is invalid.`, "SyntaxError");
597
1190
  }
@@ -659,7 +1252,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
659
1252
  });
660
1253
  function fire(type, fields = {}) {
661
1254
  const event = { type, target: socket, currentTarget: socket, isTrusted: true, bubbles: false, cancelable: false,
662
- defaultPrevented: false, timeStamp: performance.now(), preventDefault() {}, stopPropagation() {},
1255
+ defaultPrevented: false, timeStamp: pageNow(), preventDefault() {}, stopPropagation() {},
663
1256
  stopImmediatePropagation() {}, ...fields };
664
1257
  runTask(() => {
665
1258
  if (typeof socket[`on${type}`] === "function") callPage(socket[`on${type}`], socket, [event]);
@@ -711,11 +1304,12 @@ export function createPageRealm({ window, document, mode, hooks }) {
711
1304
  XMLHttpRequest.DONE = 4;
712
1305
 
713
1306
  return {
714
- fetch: pageFetch,
1307
+ // A plain function: Web IDL operations are not async functions.
1308
+ fetch: function fetch(input, init = undefined) { return pageFetch(input, init); },
715
1309
  XMLHttpRequest,
716
1310
  WebSocket,
717
1311
  Headers,
718
- Request,
1312
+ Request: PageRequest,
719
1313
  Response,
720
1314
  AbortController,
721
1315
  AbortSignal,
@@ -746,98 +1340,188 @@ export function createPageRealm({ window, document, mode, hooks }) {
746
1340
  return Interface;
747
1341
  };
748
1342
  const interfaces = {
749
- Window: make("Window", () => false),
1343
+ Window: WindowInterface,
750
1344
  EventTarget: eventTargets.EventTarget,
751
1345
  Comment: make("Comment", (node) => node.nodeType === 8),
1346
+ CDATASection: make("CDATASection", (node) => node.nodeType === 4),
1347
+ ProcessingInstruction: make("ProcessingInstruction", (node) => node.nodeType === 7),
752
1348
  DocumentType: make("DocumentType", (node) => node.nodeType === 10),
753
1349
  ShadowRoot: make("ShadowRoot", (node) => node instanceof window.ShadowRoot),
754
1350
  SVGElement: make("SVGElement", (node) => node.namespaceURI === SVG_NAMESPACE),
755
1351
  SVGSVGElement: make("SVGSVGElement", (node) => node.namespaceURI === SVG_NAMESPACE && node.localName === "svg"),
756
- HTMLUnknownElement: make("HTMLUnknownElement", () => false),
1352
+ HTMLUnknownElement: make("HTMLUnknownElement", isUnknownElement),
757
1353
  };
1354
+ interfaces.HTMLUnknownElement.prototype = unknownPrototype;
1355
+ Object.defineProperty(unknownPrototype, "constructor", { value: interfaces.HTMLUnknownElement, writable: true, configurable: true });
758
1356
  for (const [name, tags] of Object.entries(ELEMENT_INTERFACES)) {
759
- interfaces[name] = make(name, (node) => node.nodeType === 1 && tags.includes(node.tagName));
1357
+ const Interface = make(name, (node) => node.nodeType === 1 && tags.includes(node.tagName));
1358
+ Interface.prototype = elementInterfacePrototypes.get(name);
1359
+ Object.defineProperty(Interface.prototype, "constructor", { value: Interface, writable: true, configurable: true });
1360
+ if (name === "HTMLMediaElement") {
1361
+ for (const [constant, value] of Object.entries(MEDIA_CONSTANTS)) {
1362
+ Object.defineProperty(Interface, constant, { value, enumerable: true });
1363
+ }
1364
+ }
1365
+ interfaces[name] = Interface;
760
1366
  }
761
1367
  return interfaces;
762
1368
  }
763
1369
 
1370
+ class CSSRule {
1371
+ constructor(cssText = "") {
1372
+ this.cssText = String(cssText);
1373
+ }
1374
+ }
1375
+ class CSSStyleRule extends CSSRule {}
1376
+ class CSSGroupingRule extends CSSRule {}
1377
+ class CSSStyleSheet {
1378
+ constructor() {
1379
+ this.cssRules = [];
1380
+ this.disabled = false;
1381
+ this.href = null;
1382
+ this.media = { length: 0, mediaText: "", matches: () => false };
1383
+ this.ownerNode = null;
1384
+ }
1385
+ replaceSync(text) {
1386
+ const cssText = String(text);
1387
+ this.cssRules = cssText.trim() ? [new CSSStyleRule(cssText)] : [];
1388
+ }
1389
+ async replace(text) {
1390
+ this.replaceSync(text);
1391
+ return this;
1392
+ }
1393
+ insertRule(rule, index = this.cssRules.length) {
1394
+ const position = Number(index) >>> 0;
1395
+ if (position > this.cssRules.length) throw new DOMException("Index out of range", "IndexSizeError");
1396
+ this.cssRules.splice(position, 0, new CSSStyleRule(rule));
1397
+ return position;
1398
+ }
1399
+ deleteRule(index) {
1400
+ const position = Number(index) >>> 0;
1401
+ if (position >= this.cssRules.length) throw new DOMException("Index out of range", "IndexSizeError");
1402
+ this.cssRules.splice(position, 1);
1403
+ }
1404
+ }
1405
+
1406
+ // adoptedStyleSheets of the document and its shadow roots (CSSOM §6.6,
1407
+ // the DocumentOrShadowRoot mixin): one array per node, so pushing to it
1408
+ // works as on a browser's ObservableArray, and assignment replaces it.
1409
+ // The sheets are kept for pages to read back; the style engine does not
1410
+ // apply them yet.
1411
+ const adoptedSheets = new WeakMap();
1412
+ for (const [prototype, owner] of [[DocumentInterface.prototype, "Document"], [realm.shadowRootPrototype, "ShadowRoot"]]) {
1413
+ const accessors = {
1414
+ get adoptedStyleSheets() {
1415
+ const node = nodeOf(this);
1416
+ if (!node) throw new TypeError("Illegal invocation");
1417
+ let sheets = adoptedSheets.get(node);
1418
+ if (!sheets) adoptedSheets.set(node, sheets = []);
1419
+ return sheets;
1420
+ },
1421
+ set adoptedStyleSheets(value) {
1422
+ const node = nodeOf(this);
1423
+ if (!node) throw new TypeError("Illegal invocation");
1424
+ if (value === null || typeof value !== "object" || typeof value[Symbol.iterator] !== "function") {
1425
+ throw new TypeError(`Failed to set the 'adoptedStyleSheets' property on '${owner}': The provided value cannot be converted to a sequence.`);
1426
+ }
1427
+ const sheets = [...value];
1428
+ if (sheets.some((sheet) => !(sheet instanceof CSSStyleSheet))) {
1429
+ throw new TypeError(`Failed to set the 'adoptedStyleSheets' property on '${owner}': Failed to convert value to 'CSSStyleSheet'.`);
1430
+ }
1431
+ adoptedSheets.set(node, sheets);
1432
+ },
1433
+ };
1434
+ Object.defineProperty(prototype, "adoptedStyleSheets", {
1435
+ ...Object.getOwnPropertyDescriptor(accessors, "adoptedStyleSheets"), enumerable: true, configurable: true,
1436
+ });
1437
+ }
1438
+
764
1439
  const origin = safeOrigin(document.URL);
765
1440
  if (!hostStorage.has(origin)) hostStorage.set(origin, new Map());
1441
+ mediaScope = createMediaSourceScope({
1442
+ origin,
1443
+ DOMException,
1444
+ // Zero delay, as a page task, so listeners registered in the same turn
1445
+ // (right after appendBuffer or setting src) still run.
1446
+ schedule: (fn) => hostSetTimeout(() => runTask(fn), 0),
1447
+ onAppend: (source) => hooks.mediaAppended?.(source),
1448
+ onDuration: (source, duration) => hooks.mediaDuration?.(source, duration),
1449
+ });
1450
+ blobUrlFallback = mediaScope;
766
1451
 
767
1452
  const viewportValue = (key) => () => hooks.viewport()[key];
1453
+ const userAgent = hooks.userAgent();
1454
+ const chromeVersion = /(?:Chrome|Chromium)\/(\d+(?:\.\d+){0,3})/.exec(userAgent)?.[1] ?? "";
1455
+ const chromeMajor = chromeVersion.split(".")[0];
1456
+ const mobileUserAgent = /\bMobile\b/.test(userAgent);
1457
+ const uaBrands = Object.freeze([
1458
+ Object.freeze({ brand: "Not:A-Brand", version: "99" }),
1459
+ Object.freeze({ brand: "Chromium", version: chromeMajor }),
1460
+ Object.freeze({ brand: "Google Chrome", version: chromeMajor }),
1461
+ ]);
1462
+ const userAgentDataMembers = {
1463
+ brands: uaBrands,
1464
+ mobile: mobileUserAgent,
1465
+ platform: /Android/.test(userAgent) ? "Android" : /Windows/.test(userAgent) ? "Windows" : /Macintosh/.test(userAgent) ? "macOS" : "Linux",
1466
+ toJSON() { return { brands: uaBrands, mobile: mobileUserAgent, platform: this.platform }; },
1467
+ getHighEntropyValues(hints = []) {
1468
+ const values = this.toJSON();
1469
+ const available = {
1470
+ architecture: /(?:aarch64|arm64)/i.test(userAgent) ? "arm" : "x86",
1471
+ bitness: "64", model: "", platformVersion: "", uaFullVersion: chromeVersion,
1472
+ fullVersionList: uaBrands.map(({ brand, version }) => ({ brand, version: brand === "Not:A-Brand" ? version : chromeVersion })),
1473
+ wow64: false,
1474
+ };
1475
+ for (const hint of hints) if (Object.hasOwn(available, hint)) values[hint] = available[hint];
1476
+ return Promise.resolve(values);
1477
+ },
1478
+ };
768
1479
  const { location, setHref } = locationBinding(document.URL, hooks.navigate);
769
1480
  const { History, history, pushEntry } = historyBinding(location, setHref);
770
1481
  const { Event, CustomEvent, ...uiEvents } = eventConstructors(realm);
771
1482
  const observers = observerConstructors();
772
1483
  const network = networkApi();
1484
+ const userTiming = userTimingApi();
773
1485
  const windowEvents = windowEventMethods(realm, null);
1486
+ // postMessage, other frames' windows, MessageChannel (see messaging.js).
1487
+ const messaging = createMessaging({
1488
+ realm, window, pageGlobal: () => pageGlobal, runTask, callPage, DOMException, frames: hooks.frames ?? null,
1489
+ });
1490
+
1491
+ const platform = platformObjects({
1492
+ eventTargets, userAgent, userAgentDataMembers: chromeVersion ? userAgentDataMembers : null, mobile: mobileUserAgent, viewport: hooks.viewport,
1493
+ performance: userTiming.performance, storages: { local: hostStorage.get(origin), session: new Map() },
1494
+ sendBeacon: (url, data) => {
1495
+ network.fetch(url, { method: "POST", body: data, mode: "no-cors", credentials: "include", keepalive: true })
1496
+ .catch(() => {});
1497
+ return true;
1498
+ },
1499
+ });
1500
+
1501
+ const trustedTypes = trustedTypesApi();
774
1502
 
775
1503
  /** Web APIs placed on the page's global object (bindings plus hardened host utilities). */
776
1504
  const api = {
777
1505
  document: documentBinding,
778
1506
  location,
779
- navigator: {
780
- userAgent: hooks.userAgent(),
781
- language: "en-US",
782
- languages: ["en-US"],
783
- platform: "Linux",
784
- onLine: true,
785
- cookieEnabled: false,
786
- webdriver: false,
787
- hardwareConcurrency: 1,
788
- maxTouchPoints: 0,
789
- sendBeacon: (url, data) => {
790
- network.fetch(url, { method: "POST", body: data, mode: "no-cors", credentials: "include", keepalive: true })
791
- .catch(() => {});
792
- return true;
793
- },
794
- },
1507
+ navigator: platform.navigator,
795
1508
  History,
796
1509
  history,
797
- screen: {
798
- get width() {
799
- return hooks.viewport().width;
800
- },
801
- get height() {
802
- return hooks.viewport().height;
803
- },
804
- colorDepth: 24,
805
- pixelDepth: 24,
806
- },
807
- // No pinch zoom; `scale` is the --mobile scale-up (client CSS pixels per page CSS pixel).
808
- visualViewport: {
809
- offsetLeft: 0,
810
- offsetTop: 0,
811
- get pageLeft() {
812
- return hooks.viewport().scrollX;
813
- },
814
- get pageTop() {
815
- return hooks.viewport().scrollY;
816
- },
817
- get width() {
818
- return hooks.viewport().width;
819
- },
820
- get height() {
821
- return hooks.viewport().height;
822
- },
823
- get scale() {
824
- return hooks.viewport().pageScale ?? 1;
825
- },
826
- addEventListener: () => {},
827
- removeEventListener: () => {},
828
- },
829
- localStorage: storageBinding(hostStorage.get(origin)),
830
- sessionStorage: storageBinding(new Map()),
1510
+ screen: platform.screen,
1511
+ visualViewport: platform.visualViewport,
1512
+ localStorage: platform.localStorage,
1513
+ sessionStorage: platform.sessionStorage,
831
1514
  console: pageConsole,
832
1515
  setTimeout: (handler, delay = 0, ...args) => schedule(handler, delay, args, false),
833
1516
  setInterval: (handler, delay = 0, ...args) => schedule(handler, delay, args, true),
834
- clearTimeout: cancel,
835
- clearInterval: cancel,
836
- requestAnimationFrame: (callback) => schedule(() => callPage(callback, pageGlobal, [hostPerformance.now()]), 16, [], false),
837
- cancelAnimationFrame: cancel,
1517
+ // Each is its own function with its Web IDL length (handles are optional for the clears).
1518
+ clearTimeout: (id = undefined) => cancel(id),
1519
+ clearInterval: (id = undefined) => cancel(id),
1520
+ requestAnimationFrame: (callback) => schedule(() => callPage(callback, pageGlobal, [pageNow()]), 16, [], false),
1521
+ cancelAnimationFrame: (handle) => cancel(handle),
838
1522
  requestIdleCallback: (callback) => schedule(
839
1523
  () => callPage(callback, pageGlobal, [{ didTimeout: false, timeRemaining: () => 10 }]), 1, [], false),
840
- cancelIdleCallback: cancel,
1524
+ cancelIdleCallback: (handle) => cancel(handle),
841
1525
  queueMicrotask: (callback) => hostQueueMicrotask(() => runTask(() => callPage(callback, pageGlobal, []))),
842
1526
  structuredClone: (value) => hostStructuredClone(value),
843
1527
  atob: (value) => hostAtob(String(value)),
@@ -850,23 +1534,27 @@ export function createPageRealm({ window, document, mode, hooks }) {
850
1534
  // hook. Resolve it through the same module loader and network boundary as
851
1535
  // parser-inserted modules.
852
1536
  __import__: (specifier) => importModule(String(specifier), currentScript?.src || document.URL),
853
- performance: {
854
- now: () => hostPerformance.now(),
855
- timeOrigin: hostPerformance.timeOrigin,
856
- timing: navigationTiming(timingStart, timingMarks),
857
- navigation: Object.freeze({ type: 0, redirectCount: 0, TYPE_NAVIGATE: 0, TYPE_RELOAD: 1, TYPE_BACK_FORWARD: 2, TYPE_RESERVED: 255 }),
858
- mark: () => {},
859
- measure: () => {},
860
- getEntriesByType: () => [],
861
- getEntriesByName: () => [],
862
- },
863
- crypto: {
864
- getRandomValues: (array) => hostCrypto.getRandomValues(array),
865
- randomUUID: () => hostCrypto.randomUUID(),
866
- },
1537
+ performance: platform.performance,
1538
+ PerformanceObserver: userTiming.PerformanceObserver,
1539
+ crypto: platform.crypto,
1540
+ ...platform.interfaces,
1541
+ CSSStyleDeclaration: cssStyleDeclarationInterface(realm),
1542
+ ...trustedTypes.interfaces,
1543
+ trustedTypes: trustedTypes.trustedTypes,
867
1544
  Event,
868
1545
  CustomEvent,
869
1546
  ...uiEvents,
1547
+ Range: rangeConstructor(realm),
1548
+ // new Audio(src): an <audio> element, not in the document (HTML §4.8.11).
1549
+ Audio: function Audio(src = undefined) {
1550
+ if (!new.target) throw new TypeError("Constructor Audio requires 'new'");
1551
+ const audio = documentBinding.createElement("audio");
1552
+ audio.setAttribute("preload", "auto");
1553
+ if (src !== undefined) audio.setAttribute("src", String(src));
1554
+ return audio;
1555
+ },
1556
+ MediaSource: mediaScope.MediaSource,
1557
+ SourceBuffer: mediaScope.SourceBuffer,
870
1558
  ...observers,
871
1559
  ...network,
872
1560
  CSS: {
@@ -881,6 +1569,10 @@ export function createPageRealm({ window, document, mode, hooks }) {
881
1569
  return Boolean(style && (name in style || camel in style));
882
1570
  },
883
1571
  },
1572
+ CSSRule,
1573
+ CSSStyleRule,
1574
+ CSSGroupingRule,
1575
+ CSSStyleSheet,
884
1576
  NodeFilter,
885
1577
  // Collections are represented by snapshot arrays internally. Expose the
886
1578
  // standard collection prototypes used by compatibility shims; their
@@ -946,7 +1638,15 @@ export function createPageRealm({ window, document, mode, hooks }) {
946
1638
  addEventListener: windowEvents.addEventListener,
947
1639
  removeEventListener: windowEvents.removeEventListener,
948
1640
  dispatchEvent: windowEvents.dispatchEvent,
949
- getComputedStyle: (element) => computedStyleBinding(hooks.computedStyle(nodeOf(element))),
1641
+ getComputedStyle: (element) => {
1642
+ const node = nodeOf(element);
1643
+ const style = hooks.computedStyle(node);
1644
+ return computedStyleBinding(style, {
1645
+ custom: hooks.customProperties?.(node) ?? null,
1646
+ bounds: style && style.display !== "none" && node?.isConnected ? hooks.boundsOf(node) : null,
1647
+ element: node,
1648
+ });
1649
+ },
950
1650
  getSelection: () => ({
951
1651
  anchorNode: null, anchorOffset: 0, focusNode: null, focusOffset: 0,
952
1652
  isCollapsed: true, rangeCount: 0, type: "None",
@@ -968,9 +1668,9 @@ export function createPageRealm({ window, document, mode, hooks }) {
968
1668
  removeEventListener: () => {},
969
1669
  };
970
1670
  },
971
- scrollTo: (x, y) => scrollArguments(hooks, x, y, false),
972
- scroll: (x, y) => scrollArguments(hooks, x, y, false),
973
- scrollBy: (x, y) => scrollArguments(hooks, x, y, true),
1671
+ scrollTo: (x = undefined, y = undefined) => scrollArguments(hooks, x, y, false),
1672
+ scroll: (x = undefined, y = undefined) => scrollArguments(hooks, x, y, false),
1673
+ scrollBy: (x = undefined, y = undefined) => scrollArguments(hooks, x, y, true),
974
1674
  alert: () => {},
975
1675
  confirm: () => false,
976
1676
  prompt: () => null,
@@ -979,7 +1679,9 @@ export function createPageRealm({ window, document, mode, hooks }) {
979
1679
  blur: () => {},
980
1680
  close: () => {},
981
1681
  open: () => null,
982
- postMessage: () => {},
1682
+ postMessage: messaging.postMessage,
1683
+ MessageChannel: messaging.MessageChannel,
1684
+ MessagePort: messaging.MessagePort,
983
1685
  };
984
1686
  const accessors = {
985
1687
  innerWidth: viewportValue("width"),
@@ -991,38 +1693,127 @@ export function createPageRealm({ window, document, mode, hooks }) {
991
1693
  scrollY: viewportValue("scrollY"),
992
1694
  pageXOffset: viewportValue("scrollX"),
993
1695
  pageYOffset: viewportValue("scrollY"),
1696
+ // The number of child frames (window[i] is not provided).
1697
+ length: () => hooks.frames?.self.children().length ?? 0,
994
1698
  };
995
1699
 
1700
+ let workers = null;
996
1701
  if (mode === "ses") {
997
1702
  hardenBindings();
1703
+ // Dedicated workers get a compartment each, with these of the page's utilities.
1704
+ workers = createWorkers({
1705
+ realm, documentUrl: () => document.URL, fetchScript: hooks.fetchScript, runTask, callPage, reportError, DOMException,
1706
+ constructorHelper,
1707
+ pagePorts: messaging.ports,
1708
+ globals: Object.fromEntries(["console", "atob", "btoa", "TextEncoder", "TextDecoder", "URL", "URLSearchParams",
1709
+ "structuredClone", "performance", "crypto", "navigator", "fetch", "Headers", "Request", "Response", "Blob",
1710
+ "File", "FormData", "AbortController", "AbortSignal", "DOMException"]
1711
+ .filter((name) => api[name] !== undefined).map((name) => [name, api[name]])
1712
+ .concat([["Math", Math], ["Date", Date], ["Intl", Intl], ...Object.entries(HOST_LANGUAGE_GLOBALS)])),
1713
+ });
1714
+ api.Worker = workers.Worker;
998
1715
  // Host utilities are hardened before a page can see them: the engine
999
1716
  // itself uses URL, TextEncoder and Intl, so pages must not change them.
1000
- for (const value of [TextEncoder, TextDecoder, URL, URLSearchParams, Intl, Float64Array, Headers, Request, Response,
1001
- AbortController, AbortSignal, FormData, Blob, File, DOMException, ReadableStream]) {
1717
+ for (const value of [TextEncoder, TextDecoder, URL, URLSearchParams, Intl, Headers, Request, Response,
1718
+ AbortController, AbortSignal, FormData, Blob, File, DOMException,
1719
+ ...Object.values(HOST_LANGUAGE_GLOBALS), ...Object.values(HOST_STREAM_GLOBALS)]) {
1002
1720
  harden(value);
1003
1721
  }
1004
1722
  // Everything in `api` is created per document; only shared host classes
1005
1723
  // (above) and the binding classes need hardening.
1006
1724
  const compartment = new Compartment({
1007
- globals: { ...api, Math, Date, Intl, Float64Array },
1725
+ globals: { ...api, Math, Date, Intl, ...HOST_LANGUAGE_GLOBALS, ...HOST_STREAM_GLOBALS },
1008
1726
  __options__: true,
1009
1727
  });
1010
1728
  pageGlobal = compartment.globalThis;
1011
- Object.defineProperty(pageGlobal, CONSTRUCTOR_HELPER, { value: constructorHelper(compartment) });
1729
+ // SES's own globals are not Web APIs; a browser has none of them.
1730
+ // (__import__ stays, hidden: SES rewrites import() in classic scripts to it.)
1731
+ for (const name of ["Compartment", "harden", "lockdown", "assert"]) {
1732
+ if (Object.getOwnPropertyDescriptor(pageGlobal, name)?.configurable) delete pageGlobal[name];
1733
+ }
1734
+ Object.defineProperty(pageGlobal, "__import__", { enumerable: false });
1735
+ // window → Window.prototype → EventTarget.prototype, as in browsers.
1736
+ Object.setPrototypeOf(pageGlobal, WindowInterface.prototype);
1737
+ // Libraries wrap promise callbacks to propagate asynchronous context.
1738
+ // Give each page its own methods while keeping SES's shared intrinsic frozen.
1739
+ const PagePromise = compartment.evaluate("(class Promise extends globalThis.Promise {})");
1740
+ const IntrinsicPromise = pageGlobal.Promise;
1741
+ for (const name of ["then", "catch", "finally"]) {
1742
+ Object.defineProperty(PagePromise.prototype, name, {
1743
+ value: pageGlobal.Promise.prototype[name], writable: true, configurable: true,
1744
+ });
1745
+ }
1746
+ // Promise's own statics and tag, as the intrinsic has them (inherited
1747
+ // ones would work, but pages list own properties).
1748
+ for (const key of Reflect.ownKeys(IntrinsicPromise)) {
1749
+ if (["length", "name", "prototype"].includes(key)) continue;
1750
+ Object.defineProperty(PagePromise, key, { ...Object.getOwnPropertyDescriptor(IntrinsicPromise, key), configurable: true });
1751
+ }
1752
+ Object.defineProperty(PagePromise.prototype, Symbol.toStringTag, { value: "Promise", configurable: true });
1753
+ // async functions and host Web APIs still create intrinsic promises.
1754
+ Object.defineProperty(PagePromise, Symbol.hasInstance, {
1755
+ value(value) {
1756
+ return Function.prototype[Symbol.hasInstance].call(this === PagePromise ? IntrinsicPromise : this, value);
1757
+ }, configurable: true,
1758
+ });
1759
+ Object.defineProperty(PagePromise, "length", { value: 1, configurable: true });
1760
+ pageGlobal.Promise = PagePromise;
1761
+ // Namespace objects that libraries extend (reflect-metadata adds to
1762
+ // Reflect, polyfills to JSON and Math): each page gets its own mutable
1763
+ // copy with the same functions; the shared frozen intrinsics stay as they are.
1764
+ for (const name of ["Reflect", "JSON", "Math"]) {
1765
+ const shared = pageGlobal[name];
1766
+ const descriptors = Object.getOwnPropertyDescriptors(shared);
1767
+ for (const descriptor of Object.values(descriptors)) {
1768
+ descriptor.configurable = true;
1769
+ if ("value" in descriptor) descriptor.writable = true;
1770
+ }
1771
+ Object.defineProperty(pageGlobal, name, {
1772
+ value: Object.create(Object.getPrototypeOf(shared), descriptors), writable: true, configurable: true,
1773
+ });
1774
+ }
1775
+ const helper = constructorHelper(compartment);
1776
+ Object.defineProperty(pageGlobal, CONSTRUCTOR_HELPER, { value: helper });
1777
+ // Indirect eval runs code as a classic script does: in the global scope,
1778
+ // its var and function declarations becoming globals, and its own direct
1779
+ // evals rewritten. A method has no prototype, like the built-in.
1780
+ const pageEval = { eval(source) {
1781
+ const code = typeof source === "string" ? source : trustedTypes.scriptText(source);
1782
+ return code === undefined ? source : evaluate(code);
1783
+ } }.eval;
1784
+ Object.defineProperty(pageGlobal, "eval", { value: markNative(pageEval, "eval"), writable: true, configurable: true });
1785
+ Object.defineProperty(pageGlobal, DIRECT_EVAL_HELPER, { value: directEvalHelper(compartment, pageGlobal, trustedTypes.scriptText) });
1786
+ // A sloppy-mode function called without a receiver must see the page
1787
+ // global, never the engine's: the rewrite routes every such `this` here.
1788
+ const hostGlobal = globalThis;
1789
+ Object.defineProperty(pageGlobal, THIS_HELPER, {
1790
+ value: (value) => value === undefined || value === null || value === hostGlobal ? pageGlobal : value,
1791
+ });
1792
+ Object.defineProperty(pageGlobal, REFERENCE_HELPER, { value: referenceHelper(pageGlobal) });
1793
+ Object.defineProperty(pageGlobal, "Function", { value: helper.Function, writable: true, configurable: true });
1012
1794
  installGlobalAccessors(pageGlobal, accessors);
1013
1795
  for (const alias of ["window", "self", "top", "parent", "frames"]) {
1014
1796
  Object.defineProperty(pageGlobal, alias, { value: pageGlobal, writable: true, configurable: true });
1015
1797
  }
1016
- evaluate = (source) => compartment.evaluate(rewriteClassicScript(rewriteConstructorReads(source).code).code, {
1017
- sloppyGlobalsMode: true,
1798
+ defineFrameWindows(pageGlobal);
1799
+ installWindowAttributes(pageGlobal);
1800
+ evaluatePrepared = (prepared) => compartment.evaluate(prepared.code, {
1801
+ sloppyGlobalsMode: true,
1802
+ // Classic scripts are sloppy-mode code, as in browsers, once the
1803
+ // rewrite has given every sloppy function's `this` the page global.
1804
+ __sloppyCode__: prepared.parsed && !prepared.strict,
1018
1805
  __evadeImportExpressionTest__: true,
1019
1806
  __evadeHtmlCommentTest__: true,
1020
- // `eval(...)` in page code still goes through the compartment's safe
1021
- // evaluator; rejecting every source that mentions it breaks real sites.
1022
- __rejectSomeDirectEvalExpressions__: false,
1023
- });
1807
+ // `eval(...)` in page code still goes through the compartment's safe
1808
+ // evaluator; rejecting every source that mentions it breaks real sites.
1809
+ __rejectSomeDirectEvalExpressions__: false,
1810
+ });
1811
+ evaluate = (source) => {
1812
+ const classic = rewriteClassicScriptSource(source);
1813
+ return evaluatePrepared({ code: escapeSesHtmlComments(classic.code), parsed: classic.parsed, strict: classic.strict });
1814
+ };
1024
1815
  // Module code is already strict with its own scope; no global lifting.
1025
- evaluateModuleCode = (code) => compartment.evaluate(code, {
1816
+ evaluateModuleCode = (code) => compartment.evaluate(escapeSesHtmlComments(code, "module"), {
1026
1817
  __evadeImportExpressionTest__: true,
1027
1818
  __rejectSomeDirectEvalExpressions__: false,
1028
1819
  });
@@ -1036,6 +1827,9 @@ export function createPageRealm({ window, document, mode, hooks }) {
1036
1827
  for (const alias of ["window", "self", "top", "parent", "frames"]) {
1037
1828
  Object.defineProperty(globalThis, alias, { value: globalThis, writable: true, configurable: true });
1038
1829
  }
1830
+ defineFrameWindows(globalThis);
1831
+ installWindowAttributes(globalThis);
1832
+ Object.defineProperty(globalThis, REFERENCE_HELPER, { value: referenceHelper(globalThis), configurable: true });
1039
1833
  // No SES: constructors are the real ones.
1040
1834
  Object.defineProperty(globalThis, CONSTRUCTOR_HELPER, {
1041
1835
  value: (value, call = false) => call ? (...args) => Reflect.apply(value.constructor, value, args) : value.constructor,
@@ -1049,6 +1843,34 @@ export function createPageRealm({ window, document, mode, hooks }) {
1049
1843
  for (const type of WINDOW_HANDLER_EVENTS) {
1050
1844
  defineEventHandlerProperty(pageGlobal, type, () => window, () => realm);
1051
1845
  }
1846
+ // Unhandled rejections become events on the window (HTML §8.1.5.7): a page
1847
+ // that cancels unhandledrejection handles the rejection itself.
1848
+ const { PromiseRejectionEvent } = uiEvents;
1849
+ const reportedRejections = new WeakMap();
1850
+ const stopRejectionEvents = onPromiseRejection({
1851
+ unhandled: (reason, promise) => {
1852
+ if (disposed) return false;
1853
+ if (promise && typeof promise === "object") reportedRejections.set(promise, reason);
1854
+ return runTask(() => {
1855
+ const event = new PromiseRejectionEvent("unhandledrejection", { cancelable: true, promise, reason });
1856
+ pageGlobal.dispatchEvent(event);
1857
+ return event.defaultPrevented;
1858
+ }) === true;
1859
+ },
1860
+ handled: (promise) => {
1861
+ if (disposed || !reportedRejections.has(promise)) return;
1862
+ const reason = reportedRejections.get(promise);
1863
+ reportedRejections.delete(promise);
1864
+ hostSetTimeout(() => runTask(() => {
1865
+ pageGlobal.dispatchEvent(new PromiseRejectionEvent("rejectionhandled", { promise, reason }));
1866
+ }), 0);
1867
+ },
1868
+ });
1869
+ // Before any page script: the Web APIs print and name themselves as built-ins,
1870
+ // and the global has a browser's tags and enumerability.
1871
+ markNativeApi(pageGlobal);
1872
+ applyGlobalShape(pageGlobal, [Object.prototype, Function.prototype, Array.prototype, Error.prototype,
1873
+ Promise.prototype, pageGlobal.Promise?.prototype, RegExp.prototype, Date.prototype, Map.prototype, Set.prototype]);
1052
1874
 
1053
1875
  function startInsertedScripts(nodes) {
1054
1876
  for (const node of nodes) {
@@ -1082,8 +1904,8 @@ export function createPageRealm({ window, document, mode, hooks }) {
1082
1904
  return;
1083
1905
  }
1084
1906
  const pending = hooks.fetchScript(new URL(src, document.URL).href).then(
1085
- (source) => {
1086
- runClassic(script, source);
1907
+ async (source) => {
1908
+ runClassic(script, source, await preparedFor(prepareOffThread(source), source));
1087
1909
  runTask(() => script.dispatchEvent(new window.Event("load")));
1088
1910
  },
1089
1911
  (error) => {
@@ -1119,7 +1941,8 @@ export function createPageRealm({ window, document, mode, hooks }) {
1119
1941
  return record;
1120
1942
  }
1121
1943
  if (type !== null) throw new TypeError(`Unsupported module type "${type}" for ${url}`);
1122
- const { code, requests } = transformModule(mode === "ses" ? rewriteConstructorReads(source, "module").code : source);
1944
+ const { code, requests } = transformModule(mode === "ses"
1945
+ ? rewriteModuleSource(source) : source);
1123
1946
  record.fn = evaluateModuleCode(code);
1124
1947
  record.requests = requests.map((entry) => ({
1125
1948
  url: resolveModuleSpecifier(entry.specifier, url, importMap),
@@ -1208,7 +2031,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1208
2031
  };
1209
2032
  try {
1210
2033
  // A plain call: module top-level `this` is undefined.
1211
- return Promise.resolve(Reflect.apply(record.fn, undefined, [context]));
2034
+ return Promise.resolve(asIncumbent(hooks.frames?.self, () => Reflect.apply(record.fn, undefined, [context])));
1212
2035
  } catch (error) {
1213
2036
  return Promise.reject(error);
1214
2037
  }
@@ -1234,11 +2057,27 @@ export function createPageRealm({ window, document, mode, hooks }) {
1234
2057
  }
1235
2058
  }
1236
2059
 
1237
- function runClassic(script, source) {
2060
+ /**
2061
+ * Starts preparing a large classic script on the rewrite thread; resolves
2062
+ * to { source, result } (result null when it could not), or is null when
2063
+ * the script is prepared on the main thread as it runs.
2064
+ */
2065
+ function prepareOffThread(source) {
2066
+ if (!evaluatePrepared || !hooks.rewriteScript || typeof source !== "string" || source.length < OFF_THREAD_SCRIPT_SIZE) return null;
2067
+ return hooks.rewriteScript(source).then((result) => ({ source, result }), () => ({ source, result: null }));
2068
+ }
2069
+
2070
+ /** The prepared form of `source` from a prepareOffThread promise, or null. */
2071
+ async function preparedFor(pending, source) {
2072
+ const prepared = await pending;
2073
+ return prepared && prepared.source === source ? prepared.result : null;
2074
+ }
2075
+
2076
+ function runClassic(script, source, prepared = null) {
1238
2077
  const previous = currentScript;
1239
2078
  currentScript = script;
1240
2079
  try {
1241
- runTask(() => evaluate(source));
2080
+ runTask(() => (prepared ? evaluatePrepared(prepared) : evaluate(source)));
1242
2081
  } finally {
1243
2082
  currentScript = previous;
1244
2083
  }
@@ -1275,6 +2114,11 @@ export function createPageRealm({ window, document, mode, hooks }) {
1275
2114
  return pageGlobal;
1276
2115
  },
1277
2116
 
2117
+ /** The page's document object (what a same-origin frame's contentDocument is). */
2118
+ get documentBinding() {
2119
+ return documentBinding;
2120
+ },
2121
+
1278
2122
  get console() {
1279
2123
  return consoleEntries.slice();
1280
2124
  },
@@ -1285,6 +2129,14 @@ export function createPageRealm({ window, document, mode, hooks }) {
1285
2129
  * makes their `nomodule` fallbacks run instead.
1286
2130
  */
1287
2131
  async runDocumentScripts() {
2132
+ for (const source of hooks.initScripts?.() ?? []) {
2133
+ if (disposed) return;
2134
+ try {
2135
+ evaluate(source);
2136
+ } catch (error) {
2137
+ reportError(error);
2138
+ }
2139
+ }
1288
2140
  compileInlineHandlers();
1289
2141
  const all = [...document.querySelectorAll("script")];
1290
2142
  const mapScript = all.find((script) => (script.getAttribute("type") ?? "").trim().toLowerCase() === "importmap");
@@ -1314,9 +2166,17 @@ export function createPageRealm({ window, document, mode, hooks }) {
1314
2166
  // Like a preload scanner: fetch every external script (and module graph)
1315
2167
  // up front, in parallel; execution order is unchanged.
1316
2168
  const preloaded = new Map();
2169
+ // Classic scripts being prepared on the rewrite thread meanwhile.
2170
+ const prepared = new Map();
1317
2171
  for (const script of [...immediate, ...deferred, ...asynchronous]) {
1318
2172
  const src = script.getAttribute("src");
1319
- if (!src) continue;
2173
+ if (!src) {
2174
+ if ((script.getAttribute("type") ?? "").trim().toLowerCase() !== "module") {
2175
+ const pending = prepareOffThread(script.textContent ?? "");
2176
+ if (pending) prepared.set(script, pending);
2177
+ }
2178
+ continue;
2179
+ }
1320
2180
  try {
1321
2181
  const url = new URL(src, document.URL).href;
1322
2182
  if ((script.getAttribute("type") ?? "").trim().toLowerCase() === "module") {
@@ -1325,6 +2185,8 @@ export function createPageRealm({ window, document, mode, hooks }) {
1325
2185
  const source = hooks.fetchScript(url);
1326
2186
  source.catch(() => {});
1327
2187
  preloaded.set(script, source);
2188
+ const pending = source.then(prepareOffThread, () => null);
2189
+ prepared.set(script, pending);
1328
2190
  }
1329
2191
  } catch {
1330
2192
  // An unresolvable src is reported when the script runs.
@@ -1353,8 +2215,9 @@ export function createPageRealm({ window, document, mode, hooks }) {
1353
2215
  parsingHead = false;
1354
2216
  return;
1355
2217
  }
2218
+ const ready = prepared.has(script) ? await preparedFor(prepared.get(script), source) : null;
1356
2219
  try {
1357
- runClassic(script, source);
2220
+ runClassic(script, source, ready);
1358
2221
  } finally {
1359
2222
  parsingHead = false;
1360
2223
  }
@@ -1384,6 +2247,7 @@ export function createPageRealm({ window, document, mode, hooks }) {
1384
2247
  dispatch(target, event) {
1385
2248
  let prevented = false;
1386
2249
  runTask(() => {
2250
+ realm.markTrusted(event);
1387
2251
  target.dispatchEvent(event);
1388
2252
  prevented = event.defaultPrevented;
1389
2253
  });
@@ -1415,8 +2279,28 @@ export function createPageRealm({ window, document, mode, hooks }) {
1415
2279
  return runTask(() => evaluate(source));
1416
2280
  },
1417
2281
 
2282
+ /** A message posted to this document's window by a frame of the page (see messaging.js). */
2283
+ receiveMessage(message) {
2284
+ messaging.receive(message);
2285
+ },
2286
+
2287
+ /** A MediaSource object, or the one registered at a blob URL. */
2288
+ isMediaSource(value) {
2289
+ return mediaScope.isMediaSource(value);
2290
+ },
2291
+ mediaSourceAt(url) {
2292
+ return mediaScope.mediaSourceAt(url);
2293
+ },
2294
+ blobEntry(url) {
2295
+ return mediaScope.lookup(url);
2296
+ },
2297
+
1418
2298
  dispose() {
1419
2299
  disposed = true;
2300
+ stopRejectionEvents();
2301
+ if (blobUrlFallback === mediaScope) blobUrlFallback = null;
2302
+ messaging.dispose();
2303
+ workers?.dispose();
1420
2304
  // Leaving the document closes its WebSockets (1001 "going away").
1421
2305
  for (const close of openSockets) close();
1422
2306
  openSockets.clear();
@@ -1452,7 +2336,43 @@ function navigationTiming(start, marks) {
1452
2336
 
1453
2337
  function installGlobalAccessors(target, accessors) {
1454
2338
  for (const [name, get] of Object.entries(accessors)) {
1455
- Object.defineProperty(target, name, { get, configurable: true, enumerable: true });
2339
+ // [Replaceable] (innerWidth, scrollX, length...): assigning shadows the
2340
+ // attribute with a plain data property.
2341
+ Object.defineProperty(target, name, { get, set: replaceableSetter(target, name), configurable: true, enumerable: true });
2342
+ }
2343
+ }
2344
+
2345
+ function replaceableSetter(target, name) {
2346
+ return Object.getOwnPropertyDescriptor({ set [name](value) {
2347
+ Object.defineProperty(target, name, { value, writable: true, enumerable: true, configurable: true });
2348
+ } }, name).set;
2349
+ }
2350
+
2351
+ /**
2352
+ * The window's object-valued attributes as Web IDL defines them: accessors
2353
+ * on the global (window.document is a getter, not a data property), the
2354
+ * [LegacyUnforgeable] ones non-configurable, the [Replaceable] ones with a
2355
+ * setter that shadows them, and location's setter navigating.
2356
+ */
2357
+ const WINDOW_ATTRIBUTES = {
2358
+ window: "unforgeable", document: "unforgeable", top: "unforgeable", location: "unforgeable",
2359
+ self: "replaceable", frames: "replaceable", parent: "replaceable", screen: "replaceable",
2360
+ visualViewport: "replaceable", performance: "replaceable",
2361
+ customElements: "readonly", history: "readonly", navigator: "readonly", trustedTypes: "readonly",
2362
+ crypto: "readonly", localStorage: "readonly", sessionStorage: "readonly",
2363
+ };
2364
+
2365
+ function installWindowAttributes(global) {
2366
+ for (const [name, kind] of Object.entries(WINDOW_ATTRIBUTES)) {
2367
+ const descriptor = Object.getOwnPropertyDescriptor(global, name);
2368
+ if (!descriptor || !("value" in descriptor) || !descriptor.configurable) continue;
2369
+ const { value } = descriptor;
2370
+ const get = Object.getOwnPropertyDescriptor({ get [name]() { return value; } }, name).get;
2371
+ let set;
2372
+ if (kind === "replaceable") set = replaceableSetter(global, name);
2373
+ // [PutForwards=href]
2374
+ if (name === "location") set = Object.getOwnPropertyDescriptor({ set location(href) { value.href = href; } }, name).set;
2375
+ Object.defineProperty(global, name, { get, set, enumerable: true, configurable: kind !== "unforgeable" });
1456
2376
  }
1457
2377
  }
1458
2378
 
@@ -1486,22 +2406,76 @@ function constructorHelper(compartment) {
1486
2406
  const replacements = new Map();
1487
2407
  for (const [sample, keyword] of kinds) {
1488
2408
  const inert = Object.getPrototypeOf(sample).constructor;
2409
+ // Functions made from source are sloppy-mode code like classic scripts
2410
+ // (see rewriteClassicScript): Function("return this")() is the global.
1489
2411
  const Constructor = function (...args) {
1490
2412
  const body = args.length ? String(args[args.length - 1]) : "";
1491
2413
  const parameters = args.slice(0, -1).map(String).join(",");
1492
- return compartment.evaluate(`(${keyword} anonymous(${parameters}\n) {\n${body}\n})`);
2414
+ const source = `(${keyword} anonymous(${parameters}\n) {\n${body}\n})`;
2415
+ const classic = rewriteClassicScriptSource(source);
2416
+ return compartment.evaluate(escapeSesHtmlComments(classic.code), {
2417
+ sloppyGlobalsMode: true,
2418
+ __sloppyCode__: classic.parsed && !classic.strict,
2419
+ __evadeImportExpressionTest__: true,
2420
+ __evadeHtmlCommentTest__: true,
2421
+ __rejectSomeDirectEvalExpressions__: false,
2422
+ });
1493
2423
  };
1494
2424
  Object.defineProperty(Constructor, "prototype", { value: Object.getPrototypeOf(sample) });
1495
2425
  Object.defineProperty(Constructor, "name", { value: inert.name });
1496
- replacements.set(inert, keyword === "function" ? compartment.globalThis.Function : Constructor);
2426
+ Object.defineProperty(Constructor, "length", { value: 1 });
2427
+ replacements.set(inert, Constructor);
1497
2428
  }
1498
2429
  // call: `value.constructor(...)`, where an ordinary constructor keeps `value` as `this`.
1499
- return (value, call = false) => {
2430
+ const helper = (value, call = false) => {
1500
2431
  const constructor = value.constructor;
1501
2432
  const replacement = replacements.get(constructor);
1502
2433
  if (replacement) return replacement;
1503
2434
  return call && typeof constructor === "function" ? (...args) => Reflect.apply(constructor, value, args) : constructor;
1504
2435
  };
2436
+ /** The global Function constructor for the compartment. */
2437
+ helper.Function = replacements.get(Object.getPrototypeOf(function () {}).constructor);
2438
+ return helper;
2439
+ }
2440
+
2441
+ /**
2442
+ * What a direct `eval(...)` in page code calls (see rewriteDirectEval):
2443
+ * the caller's names arrive as accessors and become lexical bindings of the
2444
+ * evaluated code. As in browsers, a page that replaced `eval` gets an
2445
+ * ordinary call of its replacement, and an argument that is neither a
2446
+ * string nor a TrustedScript is returned.
2447
+ */
2448
+ function directEvalHelper(compartment, pageGlobal, scriptText) {
2449
+ const intrinsicEval = pageGlobal.eval;
2450
+ // The scope of the direct eval being evaluated: its program reads it once,
2451
+ // first (DIRECT_EVAL_SCOPE_HELPER), so nested evals only need it restored.
2452
+ let currentScope = null;
2453
+ Object.defineProperty(pageGlobal, DIRECT_EVAL_SCOPE_HELPER, { value: () => currentScope });
2454
+ return function (scope, ...args) {
2455
+ const target = pageGlobal.eval;
2456
+ if (target !== intrinsicEval) return Reflect.apply(target, undefined, args);
2457
+ const source = typeof args[0] === "string" ? args[0] : scriptText(args[0]);
2458
+ // Anything but a string or a TrustedScript is returned unchanged.
2459
+ if (source === undefined) return args[0];
2460
+ const names = Object.keys(scope);
2461
+ const prepared = prepareDirectEvalSource(source, names, scope[DIRECT_EVAL_STRICT] === true);
2462
+ const code = escapeSesHtmlComments(rewriteConstructorReads(prepared.code).code);
2463
+ // The prepared code reaches the caller's variables only through the scope
2464
+ // object, so every direct eval shares the compartment's sloppy evaluator.
2465
+ const previous = currentScope;
2466
+ currentScope = scope;
2467
+ try {
2468
+ return compartment.evaluate(code, {
2469
+ sloppyGlobalsMode: true,
2470
+ __sloppyCode__: prepared.parsed && !prepared.strict,
2471
+ __evadeImportExpressionTest__: true,
2472
+ __evadeHtmlCommentTest__: true,
2473
+ __rejectSomeDirectEvalExpressions__: false,
2474
+ });
2475
+ } finally {
2476
+ currentScope = previous;
2477
+ }
2478
+ };
1505
2479
  }
1506
2480
 
1507
2481
  /**
@@ -1601,51 +2575,284 @@ function locationBinding(href, navigate) {
1601
2575
  return { location, setHref: (value) => { href = value; } };
1602
2576
  }
1603
2577
 
1604
- function storageBinding(map) {
1605
- return {
1606
- get length() {
1607
- return map.size;
1608
- },
1609
- key: (index) => [...map.keys()][Number(index)] ?? null,
1610
- getItem: (key) => map.has(String(key)) ? map.get(String(key)) : null,
1611
- setItem: (key, value) => {
1612
- map.set(String(key), String(value));
1613
- },
1614
- removeItem: (key) => {
1615
- map.delete(String(key));
2578
+ /** navigator.platform as Chromium reports it for the user agent's OS. */
2579
+ function navigatorPlatform(userAgent) {
2580
+ if (/Android/.test(userAgent)) return "Linux armv81";
2581
+ if (/Windows/.test(userAgent)) return "Win32";
2582
+ if (/Macintosh/.test(userAgent)) return "MacIntel";
2583
+ if (/CrOS/.test(userAgent)) return "Linux x86_64";
2584
+ return `Linux ${/Linux (x86_64|aarch64|armv\w+|i686)/.exec(userAgent)?.[1] ?? "x86_64"}`;
2585
+ }
2586
+
2587
+ /**
2588
+ * navigator, screen, visualViewport, the storages, performance and crypto as
2589
+ * platform objects (see interfaces.js), with the interface objects the page
2590
+ * global exposes for them. Desktop Chromium lists five PDF viewer plugins
2591
+ * and their two MIME types whatever is installed (HTML §8.9.1.6).
2592
+ */
2593
+ function platformObjects({ eventTargets, userAgent, userAgentDataMembers, mobile, viewport, performance, storages, sendBeacon }) {
2594
+ const EventTargetPrototype = eventTargets.EventTarget.prototype;
2595
+ // An instance an event target's listeners can be added to.
2596
+ const eventTarget = (prototype) => {
2597
+ const Target = function () {};
2598
+ Target.prototype = prototype;
2599
+ return Reflect.construct(eventTargets.EventTarget, [], Target);
2600
+ };
2601
+ const targetOptions = { parent: EventTargetPrototype, base: eventTarget };
2602
+ const interfaces = {};
2603
+ const define = (name, members, options) => {
2604
+ const made = platformObject(name, members, options);
2605
+ interfaces[name] = made.Interface;
2606
+ return made;
2607
+ };
2608
+ // Indexed members are own, enumerable properties; named ones are own and not enumerable.
2609
+ const listObject = (made, slots, items, nameOf) => {
2610
+ const list = made.create(slots);
2611
+ items.forEach((item, index) => Object.defineProperty(list, index, { value: item, enumerable: true, configurable: true }));
2612
+ for (const item of items) {
2613
+ const name = nameOf(item);
2614
+ if (!Object.hasOwn(list, name)) Object.defineProperty(list, name, { value: item, configurable: true });
2615
+ }
2616
+ return list;
2617
+ };
2618
+ // Templates are merged by descriptor: spreading would read their getters.
2619
+ const members = (...parts) => Object.defineProperties({}, Object.assign({},
2620
+ ...parts.map((part) => Object.getOwnPropertyDescriptors(part))));
2621
+ const listMembers = {
2622
+ get length() { return this.items?.length ?? 0; },
2623
+ item(index) { return this.items?.[Number(index) >>> 0] ?? null; },
2624
+ namedItem(name) { return this.items?.find((item) => this.nameOf(item) === String(name)) ?? null; },
2625
+ };
2626
+
2627
+ const pdfNames = mobile ? [] : ["PDF Viewer", "Chrome PDF Viewer", "Chromium PDF Viewer", "Microsoft Edge PDF Viewer", "WebKit built-in PDF"];
2628
+ const MimeType = define("MimeType", {
2629
+ get type() { return this.type; },
2630
+ get suffixes() { return "pdf"; },
2631
+ get description() { return "Portable Document Format"; },
2632
+ get enabledPlugin() { return this.plugin(); },
2633
+ });
2634
+ const Plugin = define("Plugin", members({
2635
+ get name() { return this.name; },
2636
+ get filename() { return "internal-pdf-viewer"; },
2637
+ get description() { return "Portable Document Format"; },
2638
+ }, listMembers));
2639
+ const PluginArray = define("PluginArray", members(listMembers, { refresh() {} }));
2640
+ const MimeTypeArray = define("MimeTypeArray", listMembers);
2641
+ for (const made of [Plugin, PluginArray, MimeTypeArray]) {
2642
+ Object.defineProperty(made.prototype, Symbol.iterator, { value: Array.prototype.values, writable: true, configurable: true });
2643
+ }
2644
+ let firstPlugin = null;
2645
+ const mimeTypeList = pdfNames.length ? ["application/pdf", "text/pdf"].map((type) => MimeType.create({ type, plugin: () => firstPlugin })) : [];
2646
+ const mimeTypeOf = (item) => Object.getOwnPropertyDescriptor(MimeType.prototype, "type").get.call(item);
2647
+ const pluginList = pdfNames.map((name) => listObject(Plugin, { name, items: mimeTypeList, nameOf: mimeTypeOf }, mimeTypeList, mimeTypeOf));
2648
+ firstPlugin = pluginList[0] ?? null;
2649
+ const pluginName = (item) => Object.getOwnPropertyDescriptor(Plugin.prototype, "name").get.call(item);
2650
+ const plugins = listObject(PluginArray, { items: pluginList, nameOf: pluginName }, pluginList, pluginName);
2651
+ const mimeTypes = listObject(MimeTypeArray, { items: mimeTypeList, nameOf: mimeTypeOf }, mimeTypeList, mimeTypeOf);
2652
+
2653
+ const PermissionStatus = define("PermissionStatus", {
2654
+ get name() { return this.name; },
2655
+ get state() { return this.state; },
2656
+ onchange: null,
2657
+ }, targetOptions);
2658
+ const Permissions = define("Permissions", {
2659
+ query(descriptor) {
2660
+ if (!descriptor || typeof descriptor !== "object" || descriptor.name === undefined) {
2661
+ return Promise.reject(new TypeError("Failed to execute 'query' on 'Permissions': required member name is undefined."));
2662
+ }
2663
+ return Promise.resolve(PermissionStatus.create({ name: String(descriptor.name), state: "prompt" }));
1616
2664
  },
1617
- clear: () => map.clear(),
2665
+ });
2666
+ const NetworkInformation = define("NetworkInformation", {
2667
+ effectiveType: "4g", rtt: 50, downlink: 10, saveData: false, onchange: null,
2668
+ }, targetOptions);
2669
+ const NavigatorUAData = userAgentDataMembers ? define("NavigatorUAData", userAgentDataMembers) : null;
2670
+ const languages = Object.freeze(["en-US"]);
2671
+ const Navigator = define("Navigator", {
2672
+ userAgent,
2673
+ appCodeName: "Mozilla",
2674
+ appName: "Netscape",
2675
+ appVersion: userAgent.replace(/^Mozilla\//, ""),
2676
+ platform: navigatorPlatform(userAgent),
2677
+ product: "Gecko",
2678
+ productSub: "20030107",
2679
+ vendor: userAgentDataMembers ? "Google Inc." : /AppleWebKit\//.test(userAgent) ? "Apple Computer, Inc." : "",
2680
+ vendorSub: "",
2681
+ language: languages[0],
2682
+ languages,
2683
+ onLine: true,
2684
+ cookieEnabled: true,
2685
+ webdriver: false,
2686
+ doNotTrack: null,
2687
+ hardwareConcurrency: globalThis.navigator?.hardwareConcurrency || 4,
2688
+ deviceMemory: 8,
2689
+ maxTouchPoints: mobile ? 5 : 0,
2690
+ pdfViewerEnabled: pdfNames.length > 0,
2691
+ plugins,
2692
+ mimeTypes,
2693
+ ...(NavigatorUAData ? { userAgentData: NavigatorUAData.instance } : {}),
2694
+ connection: NetworkInformation.instance,
2695
+ permissions: Permissions.instance,
2696
+ sendBeacon(url, data = null) { return sendBeacon(url, data); },
2697
+ javaEnabled() { return false; },
2698
+ getGamepads() { return [null, null, null, null]; },
2699
+ });
2700
+
2701
+ const ScreenOrientation = define("ScreenOrientation", {
2702
+ get type() { return viewport().width >= viewport().height ? "landscape-primary" : "portrait-primary"; },
2703
+ get angle() { return 0; },
2704
+ onchange: null,
2705
+ }, targetOptions);
2706
+ const Screen = define("Screen", {
2707
+ get width() { return viewport().width; },
2708
+ get height() { return viewport().height; },
2709
+ get availWidth() { return viewport().width; },
2710
+ get availHeight() { return viewport().height; },
2711
+ availLeft: 0,
2712
+ availTop: 0,
2713
+ colorDepth: 24,
2714
+ pixelDepth: 24,
2715
+ orientation: ScreenOrientation.instance,
2716
+ isExtended: false,
2717
+ onchange: null,
2718
+ }, targetOptions);
2719
+ // No pinch zoom; `scale` is the --mobile scale-up (client CSS pixels per page CSS pixel).
2720
+ const VisualViewport = define("VisualViewport", {
2721
+ offsetLeft: 0,
2722
+ offsetTop: 0,
2723
+ get pageLeft() { return viewport().scrollX; },
2724
+ get pageTop() { return viewport().scrollY; },
2725
+ get width() { return viewport().width; },
2726
+ get height() { return viewport().height; },
2727
+ get scale() { return viewport().pageScale ?? 1; },
2728
+ onresize: null,
2729
+ onscroll: null,
2730
+ onscrollend: null,
2731
+ }, targetOptions);
2732
+ const Performance = define("Performance", performance, targetOptions);
2733
+ const Crypto = define("Crypto", {
2734
+ getRandomValues(array) { return hostCrypto.getRandomValues(array); },
2735
+ randomUUID() { return hostCrypto.randomUUID(); },
2736
+ });
2737
+ const storage = storageInterface();
2738
+ interfaces.Storage = storage.Storage;
2739
+ return {
2740
+ interfaces,
2741
+ navigator: Navigator.instance,
2742
+ screen: Screen.instance,
2743
+ visualViewport: VisualViewport.instance,
2744
+ performance: Performance.instance,
2745
+ crypto: Crypto.instance,
2746
+ localStorage: storage.create(storages.local),
2747
+ sessionStorage: storage.create(storages.session),
1618
2748
  };
1619
2749
  }
1620
2750
 
1621
2751
  /** A read-only snapshot of the few computed values the engine models. */
1622
- function computedStyleBinding(style) {
1623
- const values = style ? {
1624
- display: style.display,
1625
- position: style.position,
1626
- color: style.color,
1627
- backgroundColor: style.backgroundColor,
1628
- fontSize: `${style.fontSize}px`,
1629
- fontWeight: String(style.fontWeight),
1630
- fontFamily: (style.fontFamily ?? []).join(", "),
1631
- lineHeight: style.lineHeight == null ? "normal" : `${style.lineHeight}px`,
1632
- textAlign: style.textAlign,
1633
- whiteSpace: style.whiteSpace,
1634
- boxSizing: style.boxSizing,
1635
- zIndex: String(style.zIndex),
1636
- visibility: style.visibility ?? "visible",
1637
- opacity: String(style.opacity ?? 1),
1638
- pointerEvents: style.pointerEvents ?? "auto",
1639
- // Boxes never scroll here; a clipping box reads as hidden.
1640
- overflow: style.overflowX === "clip" || style.overflowY === "clip" ? "hidden" : "visible",
1641
- overflowX: style.overflowX === "clip" ? "hidden" : "visible",
1642
- overflowY: style.overflowY === "clip" ? "hidden" : "visible",
1643
- } : {};
2752
+ /**
2753
+ * The page's CSSStyleDeclaration for getComputedStyle (CSSOM §9): resolved
2754
+ * values for the longhands the engine computes, by camelCase name and by
2755
+ * getPropertyValue, plus the element's custom properties. Width and height
2756
+ * are used values (layout's box) when the element is rendered; percentages
2757
+ * that depend on layout read as specified otherwise.
2758
+ */
2759
+ function computedStyleBinding(style, { custom = null, bounds = null, element = null } = {}) {
2760
+ const px = (value) => `${Math.round(value * 1000) / 1000}px`;
2761
+ const length = (value) => typeof value === "number" ? px(value)
2762
+ : value?.unit === "%" ? `${value.value}%`
2763
+ : value?.unit === "math" && value.kind === "calc" ? `calc(${value.percent}% + ${value.px}px)`
2764
+ : value == null ? "auto" : String(value);
2765
+ const values = {};
2766
+ if (style) {
2767
+ const sides = ["Top", "Right", "Bottom", "Left"];
2768
+ const borderColor = (index) => style.borderColors?.[index] ?? style.color;
2769
+ const borderBox = style.boxSizing === "border-box";
2770
+ // Inline boxes (other than replaced elements) have no used width/height to report.
2771
+ if (style.display === "inline" && !["IMG", "VIDEO", "CANVAS", "IFRAME", "INPUT", "SELECT", "TEXTAREA", "BUTTON"]
2772
+ .includes(element?.tagName)) bounds = null;
2773
+ const horizontal = (style.padding[1] ?? 0) + (style.padding[3] ?? 0) + (style.borderWidths[1] ?? 0) + (style.borderWidths[3] ?? 0);
2774
+ const vertical = (style.padding[0] ?? 0) + (style.padding[2] ?? 0) + (style.borderWidths[0] ?? 0) + (style.borderWidths[2] ?? 0);
2775
+ const transform = style.transform;
2776
+ const translate = (value, size) => typeof value === "number" ? value : value?.unit === "%" ? value.value / 100 * size : 0;
2777
+ Object.assign(values, {
2778
+ display: style.display,
2779
+ position: style.position,
2780
+ float: style.float,
2781
+ color: style.color,
2782
+ backgroundColor: style.backgroundColor,
2783
+ backgroundImage: style.backgroundImage == null ? "none" : String(style.backgroundImage),
2784
+ fontSize: px(style.fontSize),
2785
+ fontWeight: String(style.fontWeight),
2786
+ fontFamily: (style.fontFamily ?? []).join(", "),
2787
+ lineHeight: style.lineHeight == null ? "normal" : px(style.lineHeight),
2788
+ textAlign: style.textAlign,
2789
+ whiteSpace: style.whiteSpace,
2790
+ verticalAlign: typeof style.verticalAlign === "number" ? px(style.verticalAlign) : String(style.verticalAlign),
2791
+ textOverflow: style.textOverflow,
2792
+ boxSizing: style.boxSizing,
2793
+ // A rendered box reads its used size; one that is not reads as computed.
2794
+ width: bounds ? px(Math.max(0, bounds.width - (borderBox ? 0 : horizontal))) : length(style.width),
2795
+ height: bounds ? px(Math.max(0, bounds.height - (borderBox ? 0 : vertical))) : length(style.height),
2796
+ minWidth: length(style.minWidth),
2797
+ maxWidth: style.maxWidth === "none" ? "none" : length(style.maxWidth),
2798
+ minHeight: length(style.minHeight),
2799
+ maxHeight: style.maxHeight === "none" ? "none" : length(style.maxHeight),
2800
+ top: length(style.inset[0]),
2801
+ right: length(style.inset[1]),
2802
+ bottom: length(style.inset[2]),
2803
+ left: length(style.inset[3]),
2804
+ zIndex: String(style.zIndex),
2805
+ flexDirection: style.flexDirection,
2806
+ flexWrap: style.flexWrap,
2807
+ flexGrow: String(style.flexGrow),
2808
+ flexShrink: String(style.flexShrink),
2809
+ flexBasis: length(style.flexBasis),
2810
+ justifyContent: style.justifyContent,
2811
+ alignContent: style.alignContent,
2812
+ alignItems: style.alignItems,
2813
+ alignSelf: style.alignSelf,
2814
+ rowGap: style.rowGap ? length(style.rowGap) : "normal",
2815
+ columnGap: style.columnGap ? length(style.columnGap) : "normal",
2816
+ borderRadius: length(style.borderRadius),
2817
+ transform: !transform ? "none"
2818
+ : `matrix(${transform.scaleX ?? 1}, 0, 0, ${transform.scaleY ?? 1}, ${translate(transform.x, bounds?.width ?? 0)}, ${translate(transform.y, bounds?.height ?? 0)})`,
2819
+ visibility: style.visibility ?? "visible",
2820
+ opacity: String(style.opacity ?? 1),
2821
+ pointerEvents: style.pointerEvents ?? "auto",
2822
+ // overflow: hidden and clip both clip; scroll containers report how they scroll.
2823
+ overflowX: style.overflowScrollX ?? (style.overflowX === "clip" ? "clip" : "visible"),
2824
+ overflowY: style.overflowScrollY ?? (style.overflowY === "clip" ? "clip" : "visible"),
2825
+ });
2826
+ values.overflow = values.overflowX === values.overflowY ? values.overflowX : `${values.overflowX} ${values.overflowY}`;
2827
+ sides.forEach((side, index) => {
2828
+ values[`margin${side}`] = length(style.margin[index]);
2829
+ values[`padding${side}`] = length(style.padding[index]);
2830
+ values[`border${side}Width`] = px(style.borderWidths[index] ?? 0);
2831
+ values[`border${side}Style`] = style.borderStyles[index] ?? "none";
2832
+ values[`border${side}Color`] = borderColor(index);
2833
+ });
2834
+ values.margin = sides.map((side) => values[`margin${side}`]).join(" ");
2835
+ values.padding = sides.map((side) => values[`padding${side}`]).join(" ");
2836
+ }
1644
2837
  const kebab = (name) => name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
1645
2838
  const byKebab = Object.fromEntries(Object.entries(values).map(([name, value]) => [kebab(name), value]));
2839
+ const names = Object.keys(byKebab);
2840
+ const customValue = (name) => {
2841
+ const value = custom?.get(name);
2842
+ return value == null ? "" : resolveVariables(value, custom);
2843
+ };
1646
2844
  return {
1647
2845
  ...values,
1648
- getPropertyValue: (name) => byKebab[String(name)] ?? "",
2846
+ length: names.length,
2847
+ item: (index) => names[Number(index)] ?? "",
2848
+ getPropertyValue: (name) => {
2849
+ const key = String(name);
2850
+ return key.startsWith("--") ? customValue(key) : byKebab[key.toLowerCase()] ?? "";
2851
+ },
2852
+ getPropertyPriority: () => "",
2853
+ get cssText() {
2854
+ return "";
2855
+ },
1649
2856
  };
1650
2857
  }
1651
2858