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