buninu-browser 0.2.1 → 0.2.3

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