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