buninu-browser 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/README.md +74 -9
- package/bunfig.toml +1 -0
- package/buninu-browser.js +4 -0
- package/index.js +5 -0
- package/lib/audio/piano.js +24 -1
- package/lib/audio/pulse-client.js +120 -12
- package/lib/cdp-server.js +19 -12
- package/lib/happy-dom/parser.js +24 -4
- package/lib/happy-dom/selector-matcher.js +83 -0
- package/lib/headless-shell.js +120 -28
- package/lib/input/hit-test.js +25 -7
- package/lib/layout/text-layout.js +365 -54
- package/lib/log.js +5 -2
- package/lib/media/byte-pipe.js +82 -0
- package/lib/media/position-audio.js +42 -0
- package/lib/network/network-service.js +326 -46
- package/lib/paint/display-list.js +322 -21
- package/lib/paint/gradient.js +11 -5
- package/lib/render-tree/index.js +62 -2
- package/lib/render-tree/svg.js +2 -1
- package/lib/renderer/bindings.js +989 -72
- package/lib/renderer/canvas-2d.js +92 -0
- package/lib/renderer/host.js +159 -9
- package/lib/renderer/interfaces.js +215 -0
- package/lib/renderer/main.js +160 -12
- package/lib/renderer/media-source.js +819 -0
- package/lib/renderer/messaging.js +411 -0
- package/lib/renderer/native-functions.js +113 -0
- package/lib/renderer/page-realm.js +1407 -200
- package/lib/renderer/page-renderer.js +1899 -394
- package/lib/renderer/rejections.js +31 -0
- package/lib/renderer/rewrite-worker.js +22 -0
- package/lib/renderer/script-rewrite.js +805 -57
- package/lib/renderer/trusted-types.js +129 -0
- package/lib/renderer/workers.js +327 -0
- package/lib/screenshot-cli.js +2 -0
- package/lib/style/computed-style.js +463 -242
- package/lib/style/selector-keys.js +302 -0
- package/lib/style/selector-traits.js +240 -0
- package/lib/user-agent.js +14 -2
- package/package.json +4 -1
- package/patches/happy-dom/0002-use-bun-platform-adapters.patch +139 -19
- package/patches/happy-dom/0006-parse-css-escapes-in-selectors.patch +115 -0
- package/patches/happy-dom/README.md +26 -0
- package/patches/ses/0002-embedder-native-functions-source-text-sloppy-code.patch +106 -0
- package/patches/ses/0003-cache-sloppy-evaluators.patch +47 -0
- package/patches/ses/README.md +4 -0
- package/test/canvas-2d.test.js +55 -0
- package/test/compositor.test.js +3 -1
- package/test/computed-style.test.js +98 -1
- package/test/display-list.test.js +74 -1
- package/test/download.test.js +35 -0
- package/test/fixtures/long-video.mp4 +0 -0
- package/test/fixtures/media-sync.js +57 -0
- package/test/happy-dom-platform-adapters.test.js +25 -0
- package/test/headless-shell.test.js +29 -0
- package/test/hit-test.test.js +25 -1
- package/test/log.test.js +4 -1
- package/test/media-source.test.js +156 -0
- package/test/media-sync.test.js +12 -0
- package/test/mobile.test.js +2 -2
- package/test/navigation.test.js +61 -0
- package/test/network-service.test.js +81 -0
- package/test/page-scripts.test.js +1094 -7
- package/test/position-audio.test.js +67 -0
- package/test/positioning.test.js +95 -1
- package/test/pulse-client.test.js +69 -0
- package/test/render-tree.test.js +22 -1
- package/test/renderer-host.test.js +5 -2
- package/test/screenshot-cli.test.js +25 -1
- package/test/script-rewrite.test.js +111 -6
- package/test/selector-keys.test.js +39 -0
- package/test/selector-traits.test.js +19 -0
- package/test/setup.js +5 -0
- package/test/text-layout.test.js +69 -1
- package/test/user-agent.test.js +60 -4
- package/vendor/happy-dom/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts +52 -22
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +18 -17
- package/vendor/ses/dist/ses.mjs +52 -6
|
@@ -9,7 +9,7 @@ index 2de8270..0c5034b 100644
|
|
|
9
9
|
import ProgressEvent from '../event/events/ProgressEvent.js';
|
|
10
10
|
import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js';
|
|
11
11
|
diff --git a/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts b/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts
|
|
12
|
-
index f2c85d6..
|
|
12
|
+
index f2c85d6..dcef8e0 100644
|
|
13
13
|
--- a/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts
|
|
14
14
|
+++ b/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts
|
|
15
15
|
@@ -4,7 +4,7 @@ import type ICanvasShape from '../../canvas/ICanvasShape.js';
|
|
@@ -21,39 +21,159 @@ index f2c85d6..961edb1 100644
|
|
|
21
21
|
import type Response from '../../fetch/Response.js';
|
|
22
22
|
import type Attr from '../attr/Attr.js';
|
|
23
23
|
|
|
24
|
-
@@ -
|
|
25
|
-
|
|
24
|
+
@@ -19,6 +19,8 @@ const DATA_URL_REGEX = /^\s*data:([^,]+)?,(.*)/;
|
|
25
|
+
export default class HTMLImageElement extends HTMLElement implements ICanvasShape {
|
|
26
|
+
public [PropertySymbol.tagName] = 'IMG';
|
|
27
|
+
public [PropertySymbol.complete] = true;
|
|
28
|
+
+ // Bumped whenever src changes: an image decoded for an earlier src is dropped.
|
|
29
|
+
+ #loadGeneration = 0;
|
|
30
|
+
public [PropertySymbol.naturalHeight] = 0;
|
|
31
|
+
public [PropertySymbol.naturalWidth] = 0;
|
|
32
|
+
public [PropertySymbol.loading] = 'auto';
|
|
33
|
+
@@ -357,6 +359,7 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
34
|
+
super[PropertySymbol.onSetAttribute](attribute, replacedAttribute);
|
|
35
|
+
|
|
36
|
+
if (attribute[PropertySymbol.name] === 'src') {
|
|
37
|
+
+ const generation = ++this.#loadGeneration;
|
|
38
|
+
const url = this.#parseUrl(attribute[PropertySymbol.value]);
|
|
39
|
+
|
|
40
|
+
if (!url) {
|
|
41
|
+
@@ -367,13 +370,13 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- if (this.#loadDataUrl(url)) {
|
|
46
|
+
+ if (this.#loadDataUrl(url, generation)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const settings = new WindowBrowserContext(this[PropertySymbol.window]).getSettings();
|
|
51
|
+
if (settings?.enableImageFileLoading) {
|
|
52
|
+
- this.#loadUrlSource(this.src);
|
|
53
|
+
+ this.#loadUrlSource(this.src, generation);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
@@ -385,6 +388,7 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
58
|
+
super[PropertySymbol.onRemoveAttribute](removedAttribute);
|
|
59
|
+
|
|
60
|
+
if (removedAttribute[PropertySymbol.name] === 'src') {
|
|
61
|
+
+ this.#loadGeneration++;
|
|
26
62
|
this[PropertySymbol.complete] = true;
|
|
63
|
+
this[PropertySymbol.naturalHeight] = 0;
|
|
64
|
+
this[PropertySymbol.naturalWidth] = 0;
|
|
65
|
+
@@ -415,24 +419,37 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
66
|
+
* Loads the image from the given data URL.
|
|
67
|
+
*
|
|
68
|
+
* @param src Source.
|
|
69
|
+
+ * @param generation The load this is (see #loadGeneration).
|
|
70
|
+
* @returns True if the image was loaded, false otherwise.
|
|
71
|
+
*/
|
|
72
|
+
- #loadDataUrl(src: string): boolean {
|
|
73
|
+
+ #loadDataUrl(src: string, generation: number): boolean {
|
|
74
|
+
const dataUrlMatch = src.match(DATA_URL_REGEX);
|
|
75
|
+
|
|
76
|
+
if (dataUrlMatch) {
|
|
77
|
+
const buffer = Buffer.from(dataUrlMatch[2], 'base64');
|
|
78
|
+
- this[PropertySymbol.complete] = true;
|
|
27
79
|
this[PropertySymbol.buffer] = buffer;
|
|
28
80
|
- try {
|
|
29
81
|
- const dimensions = BufferImageSize(buffer);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this[PropertySymbol.naturalHeight] = dimensions.height;
|
|
33
|
-
this[PropertySymbol.naturalWidth] = dimensions.width;
|
|
82
|
+
- this[PropertySymbol.naturalHeight] = dimensions.height;
|
|
83
|
+
- this[PropertySymbol.naturalWidth] = dimensions.width;
|
|
34
84
|
- } catch (e) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
+ }).catch(() => {
|
|
38
|
-
this[PropertySymbol.naturalHeight] = 0;
|
|
39
|
-
this[PropertySymbol.naturalWidth] = 0;
|
|
85
|
+
- this[PropertySymbol.naturalHeight] = 0;
|
|
86
|
+
- this[PropertySymbol.naturalWidth] = 0;
|
|
40
87
|
- }
|
|
41
88
|
- this.dispatchEvent(new this[PropertySymbol.window].Event('load'));
|
|
42
|
-
+
|
|
43
|
-
+
|
|
44
|
-
+
|
|
89
|
+
+ this[PropertySymbol.complete] = false;
|
|
90
|
+
+ // The size is read asynchronously; a src set meanwhile wins.
|
|
91
|
+
+ ImageSize(buffer).then(
|
|
92
|
+
+ (dimensions) => {
|
|
93
|
+
+ if (generation !== this.#loadGeneration) {
|
|
94
|
+
+ return;
|
|
95
|
+
+ }
|
|
96
|
+
+ this[PropertySymbol.naturalHeight] = dimensions.height;
|
|
97
|
+
+ this[PropertySymbol.naturalWidth] = dimensions.width;
|
|
98
|
+
+ this[PropertySymbol.complete] = true;
|
|
99
|
+
+ this.dispatchEvent(new this[PropertySymbol.window].Event('load'));
|
|
100
|
+
+ },
|
|
101
|
+
+ () => {
|
|
102
|
+
+ if (generation !== this.#loadGeneration) {
|
|
103
|
+
+ return;
|
|
104
|
+
+ }
|
|
105
|
+
+ this[PropertySymbol.naturalHeight] = 0;
|
|
106
|
+
+ this[PropertySymbol.naturalWidth] = 0;
|
|
107
|
+
+ this[PropertySymbol.complete] = true;
|
|
108
|
+
+ this.dispatchEvent(new this[PropertySymbol.window].Event('error'));
|
|
109
|
+
+ }
|
|
110
|
+
+ );
|
|
45
111
|
return true;
|
|
46
112
|
}
|
|
47
113
|
return false;
|
|
48
|
-
@@ -
|
|
114
|
+
@@ -442,8 +459,9 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
115
|
+
* Loads the image from the given source.
|
|
116
|
+
*
|
|
117
|
+
* @param src Source.
|
|
118
|
+
+ * @param generation The load this is (see #loadGeneration).
|
|
119
|
+
*/
|
|
120
|
+
- async #loadUrlSource(src: string): Promise<void> {
|
|
121
|
+
+ async #loadUrlSource(src: string, generation: number): Promise<void> {
|
|
122
|
+
this[PropertySymbol.complete] = false;
|
|
123
|
+
this[PropertySymbol.naturalHeight] = 0;
|
|
124
|
+
this[PropertySymbol.naturalWidth] = 0;
|
|
125
|
+
@@ -457,6 +475,10 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
126
|
+
error = <Error>e;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
+ if (generation !== this.#loadGeneration) {
|
|
130
|
+
+ return;
|
|
131
|
+
+ }
|
|
132
|
+
+
|
|
133
|
+
if (error || !response?.ok) {
|
|
134
|
+
this[PropertySymbol.complete] = true;
|
|
135
|
+
this.dispatchEvent(new this[PropertySymbol.window].Event('error'));
|
|
136
|
+
@@ -475,23 +497,34 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
137
|
+
try {
|
|
138
|
+
buffer = await response.buffer();
|
|
139
|
+
} catch (e) {
|
|
140
|
+
+ if (generation !== this.#loadGeneration) {
|
|
141
|
+
+ return;
|
|
142
|
+
+ }
|
|
143
|
+
this[PropertySymbol.complete] = true;
|
|
144
|
+
this.dispatchEvent(new this[PropertySymbol.window].Event('error'));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
- this[PropertySymbol.complete] = true;
|
|
149
|
+
+ if (generation !== this.#loadGeneration) {
|
|
150
|
+
+ return;
|
|
151
|
+
+ }
|
|
152
|
+
+
|
|
49
153
|
this[PropertySymbol.buffer] = buffer;
|
|
50
154
|
|
|
155
|
+
+ let dimensions = { width: 0, height: 0 };
|
|
51
156
|
try {
|
|
52
157
|
- const dimensions = BufferImageSize(buffer);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
158
|
+
- this[PropertySymbol.naturalHeight] = dimensions.height;
|
|
159
|
+
- this[PropertySymbol.naturalWidth] = dimensions.width;
|
|
160
|
+
+ dimensions = await ImageSize(buffer);
|
|
56
161
|
} catch (e) {
|
|
162
|
+
- this[PropertySymbol.naturalHeight] = 0;
|
|
163
|
+
- this[PropertySymbol.naturalWidth] = 0;
|
|
164
|
+
+ // Undecodable: no natural size.
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
+ if (generation !== this.#loadGeneration) {
|
|
168
|
+
+ return;
|
|
169
|
+
+ }
|
|
170
|
+
+
|
|
171
|
+
+ this[PropertySymbol.naturalHeight] = dimensions.height;
|
|
172
|
+
+ this[PropertySymbol.naturalWidth] = dimensions.width;
|
|
173
|
+
+ this[PropertySymbol.complete] = true;
|
|
174
|
+
this.dispatchEvent(new this[PropertySymbol.window].Event('load'));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
57
177
|
diff --git a/packages/happy-dom/src/web-socket/WebSocket.ts b/packages/happy-dom/src/web-socket/WebSocket.ts
|
|
58
178
|
index 3075e3a..bea5f97 100644
|
|
59
179
|
--- a/packages/happy-dom/src/web-socket/WebSocket.ts
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
diff --git a/packages/happy-dom/src/query-selector/SelectorParser.ts b/packages/happy-dom/src/query-selector/SelectorParser.ts
|
|
2
|
+
index 60cc951..62cf4b9 100644
|
|
3
|
+
--- a/packages/happy-dom/src/query-selector/SelectorParser.ts
|
|
4
|
+
+++ b/packages/happy-dom/src/query-selector/SelectorParser.ts
|
|
5
|
+
@@ -12,10 +12,11 @@ import type { TGlobalMatchFunction } from './TGlobalMatchFunction.js';
|
|
6
|
+
/**
|
|
7
|
+
* Selector group RegExp.
|
|
8
|
+
*
|
|
9
|
+
- * Group 1: Combinator (" ", ",", "+", ">", "̣~")
|
|
10
|
+
- * Group 2: Parentheses or brackets.
|
|
11
|
+
+ * Group 1: An escaped character (e.g. "\:", "\31 "), skipped.
|
|
12
|
+
+ * Group 2: Combinator (" ", ",", "+", ">", "̣~")
|
|
13
|
+
+ * Group 3: Parentheses, brackets or quotes.
|
|
14
|
+
*/
|
|
15
|
+
-const SELECTOR_GROUP_REGEXP = /(\s*[\s,+>~]\s*)|([\[\]\(\)"'])/g;
|
|
16
|
+
+const SELECTOR_GROUP_REGEXP = /(\\[0-9a-fA-F]{1,6}\s?|\\[\s\S])|(\s*[\s,+>~]\s*)|([\[\]\(\)"'])/g;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Selector RegExp.
|
|
20
|
+
@@ -45,7 +46,7 @@ const SELECTOR_GROUP_REGEXP = /(\s*[\s,+>~]\s*)|([\[\]\(\)"'])/g;
|
|
21
|
+
* Group 23: Pseudo element (e.g. "::after", "::-webkit-inner-spin-button").
|
|
22
|
+
*/
|
|
23
|
+
const SELECTOR_REGEXP =
|
|
24
|
+
- /(\*)|([a-zA-Z0-9\u00A0-\uFFFF-]+)|#(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\.)+)|\.(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\.)+)|\[(([a-zA-Z0-9-_]|\\.)+)\]|\[(([a-zA-Z0-9-_]|\\.)+)\s*([~|^$*]{0,1})\s*=\s*("([^"]*)"|'([^']*)')\s*(s|i){0,1}\]|\[(([a-zA-Z0-9-_]|\\.)+)\s*([~|^$*]{0,1})\s*=\s*(([a-zA-Z0-9\u00A0-\uFFFF_¤£-]|\\.)+)\]|:([a-zA-Z-]+)\s*\(.+\)|:([a-zA-Z-]+)|::([a-zA-Z-]+)/g;
|
|
25
|
+
+ /(\*)|((?:[a-zA-Z0-9\u00A0-\uFFFF-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|#(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|\.(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\]|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\s*([~|^$*]{0,1})\s*=\s*("((?:[^"\\]|\\[\s\S])*)"|'((?:[^'\\]|\\[\s\S])*)')\s*(s|i){0,1}\]|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\s*([~|^$*]{0,1})\s*=\s*(([a-zA-Z0-9\u00A0-\uFFFF_¤£-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\]|:([a-zA-Z-]+)\s*\(.+\)|:([a-zA-Z-]+)|::([a-zA-Z-]+)/g;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Selector pseudo RegExp.
|
|
29
|
+
@@ -55,11 +56,6 @@ const SELECTOR_REGEXP =
|
|
30
|
+
*/
|
|
31
|
+
const SELECTOR_PSEUDO_REGEXP = /:([a-zA-Z-]+)|([()])/g;
|
|
32
|
+
|
|
33
|
+
-/**
|
|
34
|
+
- * Escaped Character RegExp.
|
|
35
|
+
- */
|
|
36
|
+
-const ESCAPED_CHARACTER_REGEXP = /\\/g;
|
|
37
|
+
-
|
|
38
|
+
/**
|
|
39
|
+
* CSS Escape RegExp.
|
|
40
|
+
*
|
|
41
|
+
@@ -187,6 +183,11 @@ export default class SelectorParser {
|
|
42
|
+
|
|
43
|
+
while ((match = regExp.exec(selector))) {
|
|
44
|
+
if (match[1]) {
|
|
45
|
+
+ // An escaped character (CSS Syntax §4.3.7) is part of the selector it is in,
|
|
46
|
+
+ // never a combinator, bracket or quote.
|
|
47
|
+
+ continue;
|
|
48
|
+
+ }
|
|
49
|
+
+ if (match[2]) {
|
|
50
|
+
// Matches combinator (" ", ",", "+", ">", "̣~")
|
|
51
|
+
// We should ignore combinators that are inside parentheses, brackets or apostrophes
|
|
52
|
+
|
|
53
|
+
@@ -198,7 +199,7 @@ export default class SelectorParser {
|
|
54
|
+
) {
|
|
55
|
+
const childSelector = selector.substring(lastIndex, match.index).trim();
|
|
56
|
+
|
|
57
|
+
- switch (match[1].trim()) {
|
|
58
|
+
+ switch (match[2].trim()) {
|
|
59
|
+
case ',':
|
|
60
|
+
selectorItem = this.getSelectorGroupItem(childSelector, combinator);
|
|
61
|
+
if (!selectorItem) {
|
|
62
|
+
@@ -262,7 +263,7 @@ export default class SelectorParser {
|
|
63
|
+
} else {
|
|
64
|
+
// Matches parentheses or brackets.
|
|
65
|
+
|
|
66
|
+
- switch (match[2]) {
|
|
67
|
+
+ switch (match[3]) {
|
|
68
|
+
case '(':
|
|
69
|
+
if (depth.singleApostrophe === 0 && depth.doubleApostrophe === 0) {
|
|
70
|
+
depth.parentheses++;
|
|
71
|
+
@@ -365,22 +366,22 @@ export default class SelectorParser {
|
|
72
|
+
} else if (match[2]) {
|
|
73
|
+
// Matches tag name, e.g. "div"
|
|
74
|
+
|
|
75
|
+
- selectorItem.tagName = match[2].toUpperCase();
|
|
76
|
+
+ selectorItem.tagName = SelectorParser.cssUnescape(match[2]).toUpperCase();
|
|
77
|
+
} else if (match[3]) {
|
|
78
|
+
// Matches ID, e.g. "#id"
|
|
79
|
+
|
|
80
|
+
- selectorItem.id = match[3].replace(ESCAPED_CHARACTER_REGEXP, '');
|
|
81
|
+
+ selectorItem.id = SelectorParser.cssUnescape(match[3]);
|
|
82
|
+
} else if (match[5]) {
|
|
83
|
+
// Matches class names, e.g. ".class1"
|
|
84
|
+
|
|
85
|
+
selectorItem.classNames = selectorItem.classNames || [];
|
|
86
|
+
- selectorItem.classNames.push(match[5].replace(ESCAPED_CHARACTER_REGEXP, ''));
|
|
87
|
+
+ selectorItem.classNames.push(SelectorParser.cssUnescape(match[5]));
|
|
88
|
+
} else if (match[7]) {
|
|
89
|
+
// Matches attributes without value, e.g. [attr]
|
|
90
|
+
|
|
91
|
+
selectorItem.attributes = selectorItem.attributes || [];
|
|
92
|
+
selectorItem.attributes.push({
|
|
93
|
+
- name: match[7].replace(ESCAPED_CHARACTER_REGEXP, ''),
|
|
94
|
+
+ name: SelectorParser.cssUnescape(match[7]),
|
|
95
|
+
operator: null,
|
|
96
|
+
value: null,
|
|
97
|
+
modifier: null,
|
|
98
|
+
@@ -393,7 +394,7 @@ export default class SelectorParser {
|
|
99
|
+
const unescapedValue = SelectorParser.cssUnescape(value);
|
|
100
|
+
selectorItem.attributes = selectorItem.attributes || [];
|
|
101
|
+
selectorItem.attributes.push({
|
|
102
|
+
- name: match[9].replace(ESCAPED_CHARACTER_REGEXP, ''),
|
|
103
|
+
+ name: SelectorParser.cssUnescape(match[9]),
|
|
104
|
+
operator: match[11] || null,
|
|
105
|
+
value: unescapedValue,
|
|
106
|
+
modifier: <'s'>match[15] || null,
|
|
107
|
+
@@ -409,7 +410,7 @@ export default class SelectorParser {
|
|
108
|
+
const unescapedValue = SelectorParser.cssUnescape(match[19]);
|
|
109
|
+
selectorItem.attributes = selectorItem.attributes || [];
|
|
110
|
+
selectorItem.attributes.push({
|
|
111
|
+
- name: match[16].replace(ESCAPED_CHARACTER_REGEXP, ''),
|
|
112
|
+
+ name: SelectorParser.cssUnescape(match[16]),
|
|
113
|
+
operator: match[18] || null,
|
|
114
|
+
value: unescapedValue,
|
|
115
|
+
modifier: null,
|
|
@@ -21,6 +21,8 @@ git -C vendor/happy-dom apply --check ../../patches/happy-dom/0004-insert-adjace
|
|
|
21
21
|
git -C vendor/happy-dom apply ../../patches/happy-dom/0004-insert-adjacent-html-in-order.patch
|
|
22
22
|
git -C vendor/happy-dom apply --check ../../patches/happy-dom/0005-invalidate-matches-on-structure-changes.patch
|
|
23
23
|
git -C vendor/happy-dom apply ../../patches/happy-dom/0005-invalidate-matches-on-structure-changes.patch
|
|
24
|
+
git -C vendor/happy-dom apply --check ../../patches/happy-dom/0006-parse-css-escapes-in-selectors.patch
|
|
25
|
+
git -C vendor/happy-dom apply ../../patches/happy-dom/0006-parse-css-escapes-in-selectors.patch
|
|
24
26
|
```
|
|
25
27
|
|
|
26
28
|
For the already-patched vendored tree, verify that the recorded patch can be
|
|
@@ -32,6 +34,7 @@ git -C vendor/happy-dom apply --reverse --check ../../patches/happy-dom/0002-use
|
|
|
32
34
|
git -C vendor/happy-dom apply --reverse --check ../../patches/happy-dom/0003-defer-script-events-to-renderer.patch
|
|
33
35
|
git -C vendor/happy-dom apply --reverse --check ../../patches/happy-dom/0004-insert-adjacent-html-in-order.patch
|
|
34
36
|
git -C vendor/happy-dom apply --reverse --check ../../patches/happy-dom/0005-invalidate-matches-on-structure-changes.patch
|
|
37
|
+
git -C vendor/happy-dom apply --reverse --check ../../patches/happy-dom/0006-parse-css-escapes-in-selectors.patch
|
|
35
38
|
```
|
|
36
39
|
|
|
37
40
|
## Patch manifest
|
|
@@ -43,6 +46,29 @@ git -C vendor/happy-dom apply --reverse --check ../../patches/happy-dom/0005-inv
|
|
|
43
46
|
| `0003-defer-script-events-to-renderer.patch` | `packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts` | Suppress Happy DOM's synthetic load/error event when its own script loader is disabled; the renderer fetches and executes scripts and dispatches the real outcome itself. | `bun test test/page-scripts.test.js` (`page-inserted scripts run; innerHTML scripts do not; on* handlers fire`) |
|
|
44
47
|
| `0004-insert-adjacent-html-in-order.patch` | `packages/happy-dom/src/nodes/element/Element.ts` | `insertAdjacentHTML` inserted the parsed nodes one at a time, which reversed their order at `afterbegin` and `afterend`; insert the parsed fragment as a whole. | `bun test test/page-scripts.test.js` (`insertAdjacentHTML keeps the order of the inserted nodes at every position`) |
|
|
45
48
|
| `0005-invalidate-matches-on-structure-changes.patch` | `packages/happy-dom/src/query-selector/QuerySelector.ts` | `Element.matches()` cached results that were invalidated only by changes to the elements the matcher visited, so structural pseudo-classes and sibling combinators stayed stale after a child list change, and `:has()` after any change in the subtree; register the parent of every visited element, and do not cache `:has()` matches. | `bun test test/happy-dom-matches.test.js` |
|
|
49
|
+
| `0006-parse-css-escapes-in-selectors.patch` | `packages/happy-dom/src/query-selector/SelectorParser.ts` | The group scan treated a comma, space, quote, or parenthesis that belonged to a CSS escape (`\,`, `\31 `, `\"`, `\(`) as a combinator or grouping character, and a type selector stopped at the first backslash, so `\64 iv` and `my\-el` did not parse. Escapes are consumed first (CSS Syntax §4.3.7) and a type selector is unescaped with the existing `cssUnescape` before it is uppercased. Identifiers, classes, and attribute values already accepted a backslash; this patch does not change how those are decoded. | `bun test test/page-scripts.test.js -t "selectors with CSS escapes"` |
|
|
50
|
+
|
|
51
|
+
### `0006` against upstream `SelectorParser.ts`
|
|
52
|
+
|
|
53
|
+
Upstream `v20.14.5` splits a selector with `SELECTOR_GROUP_REGEXP`. Its groups
|
|
54
|
+
are a combinator (` `, `,`, `+`, `>`, `~`) and a grouping character (`(`, `)`,
|
|
55
|
+
`[`, `]`, `"` or `'`). A backslash is not special there, so the character after
|
|
56
|
+
it is what the scan sees. The type-selector alternative is
|
|
57
|
+
`[a-zA-Z0-9\u00A0-\uFFFF-]+`, which also stops at `\`.
|
|
58
|
+
|
|
59
|
+
| Selector | Upstream | With `0006` |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `style\=\"\"` | each `"` changes the quote depth, then the type selector stops at `\`, so the selector is rejected | both escapes stay in one type selector; `cssUnescape` yields `style=""` |
|
|
62
|
+
| `.x\,y > span` | the comma starts another selector in the list | `\,` stays in the class; `>` is still the child combinator |
|
|
63
|
+
| `\64 iv` | the space after the hex escape is a descendant combinator, and `\` is not a type-selector character | `\64 ` is one escape (U+0064), so the type is `div` |
|
|
64
|
+
| `my\-el` | the type selector matches `my` and then fails on `\-el` | the type is `my-el` |
|
|
65
|
+
|
|
66
|
+
The patched group expression matches an escape first
|
|
67
|
+
(`\[0-9a-fA-F]{1,6}\s?` or `\[\s\S]`) and skips it. Combinators and grouping
|
|
68
|
+
characters move to groups 2 and 3. A type selector may contain the same
|
|
69
|
+
escapes and is passed through `cssUnescape`, the helper upstream already uses
|
|
70
|
+
for attribute values, before `toUpperCase()`. Classes and ids still drop a
|
|
71
|
+
backslash with `replace(/\\/g, '')`; hex escapes in those are unchanged.
|
|
46
72
|
|
|
47
73
|
Keep each local Happy DOM change in a separate numbered patch and add it to
|
|
48
74
|
this table with its reason and focused test. When updating Happy DOM, regenerate
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
diff --git a/dist/ses.mjs b/dist/ses.mjs
|
|
2
|
+
index 8bda46b..d7b18ff 100644
|
|
3
|
+
--- a/dist/ses.mjs
|
|
4
|
+
+++ b/dist/ses.mjs
|
|
5
|
+
@@ -9568,7 +9568,7 @@ function buildOptimizer(constants, name) {
|
|
6
|
+
${globalObjectOptimizer}
|
|
7
|
+
${moduleLexicalOptimizer}
|
|
8
|
+
return function() {
|
|
9
|
+
- 'use strict';
|
|
10
|
+
+ ${context.sloppyCode ? '' : "'use strict';"}
|
|
11
|
+
return eval(arguments[0]);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
@@ -9611,6 +9611,7 @@ const { Fail } = assert;
|
|
15
|
+
moduleLexicals = {},
|
|
16
|
+
globalTransforms = [],
|
|
17
|
+
sloppyGlobalsMode = false,
|
|
18
|
+
+ sloppyCode = false,
|
|
19
|
+
}) => {
|
|
20
|
+
const scopeTerminator = sloppyGlobalsMode
|
|
21
|
+
? createSloppyGlobalsScopeTerminator(globalObject)
|
|
22
|
+
@@ -9623,6 +9624,7 @@ const { Fail } = assert;
|
|
23
|
+
moduleLexicals,
|
|
24
|
+
globalObject,
|
|
25
|
+
scopeTerminator,
|
|
26
|
+
+ sloppyCode,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Defer creating the actual evaluator to first use.
|
|
30
|
+
@@ -9715,6 +9717,14 @@ const nativeSuffix = ') { [native code] }';
|
|
31
|
+
// use this top level state so that multiple calls to `tameFunctionToString` are
|
|
32
|
+
// idempotent, rather than creating redundant indirections.
|
|
33
|
+
let markVirtualizedNativeFunction;
|
|
34
|
+
+let sourceTextTransform;
|
|
35
|
+
+// Taken (and deleted) by the embedder, like the native-function marker.
|
|
36
|
+
+Object.defineProperty(globalThis, Symbol.for('ses.setFunctionSourceTextTransform'), {
|
|
37
|
+
+ value: transform => {
|
|
38
|
+
+ sourceTextTransform = transform;
|
|
39
|
+
+ },
|
|
40
|
+
+ configurable: true,
|
|
41
|
+
+});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Replace `Function.prototype.toString` with one that recognizes
|
|
45
|
+
@@ -9726,7 +9736,10 @@ let markVirtualizedNativeFunction;
|
|
46
|
+
|
|
47
|
+
const tamingMethods = {
|
|
48
|
+
toString() {
|
|
49
|
+
- const str = functionToString(this);
|
|
50
|
+
+ let str = functionToString(this);
|
|
51
|
+
+ // The embedder may map the text of source it rewrote back to the
|
|
52
|
+
+ // text the page wrote (see the registered symbol below).
|
|
53
|
+
+ if (sourceTextTransform !== undefined) str = sourceTextTransform(str);
|
|
54
|
+
if (
|
|
55
|
+
stringEndsWith(str, nativeSuffix) ||
|
|
56
|
+
!weaksetHas(virtualizedNativeFunctions, this)
|
|
57
|
+
@@ -9744,6 +9757,8 @@ let markVirtualizedNativeFunction;
|
|
58
|
+
markVirtualizedNativeFunction = freeze(func =>
|
|
59
|
+
weaksetAdd(virtualizedNativeFunctions, func),
|
|
60
|
+
);
|
|
61
|
+
+ // Function.prototype.toString is itself a built-in function.
|
|
62
|
+
+ markVirtualizedNativeFunction(tamingMethods.toString);
|
|
63
|
+
}
|
|
64
|
+
return markVirtualizedNativeFunction;
|
|
65
|
+
};$h͏_once.tameFunctionToString(tameFunctionToString);
|
|
66
|
+
@@ -12523,12 +12538,15 @@ const { quote: q } = assert;
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
const provideCompartmentEvaluator = (compartmentFields, options) => {
|
|
70
|
+
- const { sloppyGlobalsMode = false, __moduleShimLexicals__ = undefined } =
|
|
71
|
+
- options;
|
|
72
|
+
+ const {
|
|
73
|
+
+ sloppyGlobalsMode = false,
|
|
74
|
+
+ __moduleShimLexicals__ = undefined,
|
|
75
|
+
+ __sloppyCode__ = false,
|
|
76
|
+
+ } = options;
|
|
77
|
+
|
|
78
|
+
let safeEvaluate;
|
|
79
|
+
|
|
80
|
+
- if (__moduleShimLexicals__ === undefined && !sloppyGlobalsMode) {
|
|
81
|
+
+ if (__moduleShimLexicals__ === undefined && !sloppyGlobalsMode && !__sloppyCode__) {
|
|
82
|
+
({ safeEvaluate } = compartmentFields);
|
|
83
|
+
} else {
|
|
84
|
+
// The scope proxy or global lexicals are different from the
|
|
85
|
+
@@ -12560,6 +12578,7 @@ const { quote: q } = assert;
|
|
86
|
+
moduleLexicals,
|
|
87
|
+
globalTransforms,
|
|
88
|
+
sloppyGlobalsMode,
|
|
89
|
+
+ sloppyCode: __sloppyCode__,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@@ -15138,6 +15157,13 @@ globalThis.repairIntrinsics = options => {
|
|
94
|
+
|
|
95
|
+
const markVirtualizedNativeFunction = tameFunctionToString();
|
|
96
|
+
|
|
97
|
+
+// The embedder takes this once (and deletes it) to present its own
|
|
98
|
+
+// host-implemented Web APIs as built-in functions, as browsers do.
|
|
99
|
+
+Object.defineProperty(globalThis, Symbol.for('ses.markVirtualizedNativeFunction'), {
|
|
100
|
+
+ value: markVirtualizedNativeFunction,
|
|
101
|
+
+ configurable: true,
|
|
102
|
+
+});
|
|
103
|
+
+
|
|
104
|
+
const muteReporter = chooseReporter('none');
|
|
105
|
+
|
|
106
|
+
// @ts-ignore Compartment is definitely on globalThis.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
diff --git a/dist/ses.mjs b/dist/ses.mjs
|
|
2
|
+
index d7b18ff..31d23ad 100644
|
|
3
|
+
--- a/dist/ses.mjs
|
|
4
|
+
+++ b/dist/ses.mjs
|
|
5
|
+
@@ -12537,7 +12537,13 @@ const { quote: q } = assert;
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
- const provideCompartmentEvaluator = (compartmentFields, options) => {
|
|
10
|
+
+ // Evaluators for sloppyGlobalsMode / __sloppyCode__, one per compartment and
|
|
11
|
+
+// combination: building one scans the global object and compiles a new
|
|
12
|
+
+// evaluator function, which made every such evaluate() call cost hundreds of
|
|
13
|
+
+// microseconds.
|
|
14
|
+
+const sloppyEvaluators = new WeakMap();
|
|
15
|
+
+
|
|
16
|
+
+const provideCompartmentEvaluator = (compartmentFields, options) => {
|
|
17
|
+
const {
|
|
18
|
+
sloppyGlobalsMode = false,
|
|
19
|
+
__moduleShimLexicals__ = undefined,
|
|
20
|
+
@@ -12546,6 +12552,13 @@ const { quote: q } = assert;
|
|
21
|
+
|
|
22
|
+
let safeEvaluate;
|
|
23
|
+
|
|
24
|
+
+ const cacheKey = __moduleShimLexicals__ === undefined
|
|
25
|
+
+ ? `${Boolean(sloppyGlobalsMode)}:${Boolean(__sloppyCode__)}` : undefined;
|
|
26
|
+
+ let cached = sloppyEvaluators.get(compartmentFields);
|
|
27
|
+
+ if (cacheKey !== undefined && cached && cached.has(cacheKey)) {
|
|
28
|
+
+ return { safeEvaluate: cached.get(cacheKey) };
|
|
29
|
+
+ }
|
|
30
|
+
+
|
|
31
|
+
if (__moduleShimLexicals__ === undefined && !sloppyGlobalsMode && !__sloppyCode__) {
|
|
32
|
+
({ safeEvaluate } = compartmentFields);
|
|
33
|
+
} else {
|
|
34
|
+
@@ -12580,6 +12593,13 @@ const { quote: q } = assert;
|
|
35
|
+
sloppyGlobalsMode,
|
|
36
|
+
sloppyCode: __sloppyCode__,
|
|
37
|
+
}));
|
|
38
|
+
+ if (cacheKey !== undefined) {
|
|
39
|
+
+ if (!cached) {
|
|
40
|
+
+ cached = new Map();
|
|
41
|
+
+ sloppyEvaluators.set(compartmentFields, cached);
|
|
42
|
+
+ }
|
|
43
|
+
+ cached.set(cacheKey, safeEvaluate);
|
|
44
|
+
+ }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { safeEvaluate };
|
package/patches/ses/README.md
CHANGED
|
@@ -6,11 +6,15 @@ relative to `vendor/ses`.
|
|
|
6
6
|
For the already-patched tree, verify the recorded patch without changing files:
|
|
7
7
|
|
|
8
8
|
```sh
|
|
9
|
+
git -C vendor/ses apply --reverse --check ../../patches/ses/0003-cache-sloppy-evaluators.patch
|
|
10
|
+
git -C vendor/ses apply --reverse --check ../../patches/ses/0002-embedder-native-functions-source-text-sloppy-code.patch
|
|
9
11
|
git -C vendor/ses apply --reverse --check ../../patches/ses/0001-allow-regexp-own-tostring.patch
|
|
10
12
|
```
|
|
11
13
|
|
|
12
14
|
| Patch | Modified file | Reason | Verification |
|
|
13
15
|
|---|---|---|---|
|
|
14
16
|
| `0001-allow-regexp-own-tostring.patch` | `dist/ses.mjs` | Allow a page script to assign an own `toString` method to a `RegExp` instance despite the frozen prototype. This does not make the prototype writable. | `bun test test/page-scripts.test.js` |
|
|
17
|
+
| `0002-embedder-native-functions-source-text-sloppy-code.patch` | `dist/ses.mjs` | Three embedder hooks for browser fidelity: (1) the toString taming's `markVirtualizedNativeFunction` is left on the host global under `Symbol.for("ses.markVirtualizedNativeFunction")` (taken and deleted by `lib/renderer/native-functions.js`) so Web APIs print as `[native code]`, and the taming's own `toString` is marked native; (2) a `Symbol.for("ses.setFunctionSourceTextTransform")` setter lets the embedder map rewritten source text back to the page's text in `Function.prototype.toString`; (3) an `__sloppyCode__` evaluate option drops the evaluator's `'use strict'`, so classic scripts run as sloppy-mode code. Page code only gets (3) for sources the rewriter parsed, which routes every sloppy `this` through a helper that never yields the host global. | `bun test test/page-scripts.test.js test/script-rewrite.test.js` |
|
|
18
|
+
| `0003-cache-sloppy-evaluators.patch` | `dist/ses.mjs` | `evaluate()` with `sloppyGlobalsMode` or `__sloppyCode__` built a new safe evaluator on every call (scanning the global object for constants and compiling a new evaluator function), which made each page `eval`, `Function` and classic script cost hundreds of microseconds. The evaluator is now kept per compartment and option combination; it only binds non-writable, non-configurable globals as constants, which cannot change. `__moduleShimLexicals__` evaluations are not cached. | `bun test test/page-scripts.test.js test/script-rewrite.test.js` |
|
|
15
19
|
|
|
16
20
|
Keep future local SES changes in numbered patches and update this manifest.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { beforeAll, expect, test } from "bun:test";
|
|
2
|
+
import CanvasKitInit from "../usr/lib/canvaskit/canvaskit.js";
|
|
3
|
+
import { canvasContext, resetCanvas, setCanvasKit } from "../lib/renderer/canvas-2d.js";
|
|
4
|
+
|
|
5
|
+
beforeAll(async () => {
|
|
6
|
+
const wasmBinary = await Bun.file(new URL("../usr/lib/canvaskit/canvaskit.wasm", import.meta.url)).arrayBuffer();
|
|
7
|
+
setCanvasKit(await CanvasKitInit({ wasmBinary }));
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function canvas(width = 1, height = 1) {
|
|
11
|
+
return { width, height };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The probe web-animations-next-lite uses to accept a CSS colour.
|
|
16
|
+
* CanvasKit turns a string it cannot parse into black, so the two reads match
|
|
17
|
+
* and the sample is opaque black. A string it can parse reads back the same
|
|
18
|
+
* way both times.
|
|
19
|
+
*/
|
|
20
|
+
function sample(context, color) {
|
|
21
|
+
context.fillStyle = "#000";
|
|
22
|
+
context.fillStyle = color;
|
|
23
|
+
const first = context.fillStyle;
|
|
24
|
+
context.fillStyle = "#fff";
|
|
25
|
+
context.fillStyle = color;
|
|
26
|
+
if (first !== context.fillStyle) return null;
|
|
27
|
+
context.fillRect(0, 0, 1, 1);
|
|
28
|
+
const data = [...context.getImageData(0, 0, 1, 1).data];
|
|
29
|
+
context.clearRect(0, 0, 1, 1);
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test("a colour reads back stably, and one CanvasKit cannot parse is black", () => {
|
|
34
|
+
const context = canvasContext(canvas(), "2d");
|
|
35
|
+
expect(context.fillStyle).toBe("#000000");
|
|
36
|
+
expect(sample(context, "not-a-color")).toEqual([0, 0, 0, 255]);
|
|
37
|
+
expect(sample(context, "red")).toEqual([255, 0, 0, 255]);
|
|
38
|
+
expect(sample(context, "rgba(1, 2, 3, 0.5)")).toEqual([1, 2, 3, 128]);
|
|
39
|
+
expect(context.getImageData(0, 0, 1, 1).data.length).toBe(4);
|
|
40
|
+
expect(context.getImageData(0, 0, 1, 1).data[3]).toBe(0);
|
|
41
|
+
expect(canvasContext(canvas(), "webgl")).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("the same canvas keeps one context, and changing width clears it", () => {
|
|
45
|
+
const node = canvas(2, 2);
|
|
46
|
+
const context = canvasContext(node, "2d");
|
|
47
|
+
expect(canvasContext(node, "2d")).toBe(context);
|
|
48
|
+
context.fillStyle = "blue";
|
|
49
|
+
context.fillRect(0, 0, 2, 2);
|
|
50
|
+
expect(context.getImageData(1, 1, 1, 1).data[2]).toBe(255);
|
|
51
|
+
node.width = 1;
|
|
52
|
+
resetCanvas(node);
|
|
53
|
+
expect(context.fillStyle).toBe("#000000");
|
|
54
|
+
expect([...context.getImageData(0, 0, 1, 1).data]).toEqual([0, 0, 0, 0]);
|
|
55
|
+
});
|
package/test/compositor.test.js
CHANGED
|
@@ -106,7 +106,9 @@ test("a preview is shown while stylesheets load, then the styled page, then scri
|
|
|
106
106
|
try {
|
|
107
107
|
await context.resize(320, 200, 1);
|
|
108
108
|
await context.navigate(`http://127.0.0.1:${server.port}/`);
|
|
109
|
-
// navigate returned at the first paint: the stylesheet is still pending
|
|
109
|
+
// navigate returned at the first paint: the stylesheet is still pending
|
|
110
|
+
// (held by the server; under load its request may only now be arriving).
|
|
111
|
+
for (const deadline = Date.now() + 10_000; !releaseStyles && Date.now() < deadline;) await Bun.sleep(20);
|
|
110
112
|
expect(releaseStyles).toBeFunction();
|
|
111
113
|
expect(events).toEqual([]);
|
|
112
114
|
expect(context.lifecycleState()).toEqual([]);
|