buninu-browser 0.1.0 → 0.2.1
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 +17 -0
- package/LICENSES/brotli.txt +19 -0
- package/README.md +41 -7
- package/THIRD_PARTY_NOTICES.md +26 -4
- package/buninu-browser.js +10 -2
- package/lib/audio/pcm.js +16 -0
- package/lib/audio/piano.js +109 -0
- package/lib/audio/pulse-client.js +413 -0
- package/lib/cdp-server.js +28 -7
- package/lib/document-types.js +308 -0
- package/lib/download-directory.js +28 -0
- package/lib/download.js +63 -0
- package/lib/happy-dom/selector-matcher.js +83 -0
- package/lib/headless-shell.js +165 -26
- package/lib/input/hit-test.js +57 -8
- package/lib/layout/text-layout.js +290 -41
- package/lib/network/network-service.js +177 -40
- package/lib/network/resolver.js +16 -0
- package/lib/paint/display-list.js +25 -10
- package/lib/render-tree/index.js +58 -0
- package/lib/renderer/bindings.js +543 -55
- package/lib/renderer/flags.js +8 -5
- package/lib/renderer/host.js +103 -7
- package/lib/renderer/main.js +35 -3
- package/lib/renderer/module-transform.js +30 -23
- package/lib/renderer/page-realm.js +228 -24
- package/lib/renderer/page-renderer.js +1264 -221
- package/lib/screenshot-cli.js +61 -0
- package/lib/style/computed-style.js +558 -229
- package/lib/style/selector-keys.js +302 -0
- package/lib/style/selector-traits.js +240 -0
- package/lib/user-agent.js +12 -0
- package/package.json +1 -1
- package/patches/happy-dom/0003-defer-script-events-to-renderer.patch +40 -0
- package/patches/happy-dom/0004-insert-adjacent-html-in-order.patch +19 -0
- package/patches/happy-dom/0005-invalidate-matches-on-structure-changes.patch +43 -0
- package/patches/happy-dom/README.md +12 -0
- package/patches/ses/0001-allow-regexp-own-tostring.patch +13 -0
- package/patches/ses/README.md +16 -0
- package/test/computed-style.test.js +113 -1
- package/test/document-types.test.js +111 -0
- package/test/download.test.js +192 -0
- package/test/fixtures/tone.mp3 +0 -0
- package/test/flex.test.js +11 -0
- package/test/happy-dom-matches.test.js +25 -0
- package/test/headless-shell.test.js +56 -0
- package/test/hit-test.test.js +37 -0
- package/test/log.test.js +4 -1
- package/test/network-service.test.js +67 -0
- package/test/page-scripts.test.js +437 -29
- package/test/piano.test.js +65 -0
- package/test/positioning.test.js +11 -0
- package/test/pulse-client.test.js +130 -0
- package/test/render-tree.test.js +36 -1
- package/test/resolver.test.js +12 -1
- package/test/screenshot-cli.test.js +19 -0
- package/test/selector-keys.test.js +39 -0
- package/test/selector-traits.test.js +19 -0
- package/test/text-layout.test.js +91 -1
- package/test/user-agent.test.js +42 -0
- package/vendor/happy-dom/packages/happy-dom/src/nodes/element/Element.ts +3 -6
- package/vendor/happy-dom/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +2 -20
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/QuerySelector.ts +15 -3
- package/vendor/ses/dist/ses.mjs +2 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keys a stylesheet selector is indexed by (see RuleIndex in
|
|
3
|
+
* computed-style.js): the subject compound's bucket keys, and the keys its
|
|
4
|
+
* ancestors must carry for the ancestor filter.
|
|
5
|
+
*
|
|
6
|
+
* Selectors are scanned by character code and position: names are cut out
|
|
7
|
+
* of the selector only for the keys returned, without regular expressions or
|
|
8
|
+
* intermediate substrings, as tens of thousands of selectors are indexed for
|
|
9
|
+
* every new document.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const BACKSLASH = 0x5c, QUOTE = 0x22, APOSTROPHE = 0x27, OPEN_PAREN = 0x28, CLOSE_PAREN = 0x29;
|
|
13
|
+
const OPEN_BRACKET = 0x5b, CLOSE_BRACKET = 0x5d, SPACE = 0x20, TAB = 0x09, LINE_FEED = 0x0a, CARRIAGE_RETURN = 0x0d;
|
|
14
|
+
const FORM_FEED = 0x0c, GREATER = 0x3e, PLUS = 0x2b, TILDE = 0x7e, HASH = 0x23, PERIOD = 0x2e, COLON = 0x3a;
|
|
15
|
+
const COMMA = 0x2c, PIPE = 0x7c, HYPHEN = 0x2d, UNDERSCORE = 0x5f;
|
|
16
|
+
|
|
17
|
+
// The selector being analyzed; the functions below take positions in it.
|
|
18
|
+
let text = "";
|
|
19
|
+
|
|
20
|
+
// Scratch results, reused instead of allocated per selector and compound:
|
|
21
|
+
// one per level of :is() nesting, as an outer result stays in use while the
|
|
22
|
+
// alternatives inside it are analyzed.
|
|
23
|
+
const partsPool = [];
|
|
24
|
+
const keysPool = [];
|
|
25
|
+
|
|
26
|
+
function scratchParts(level) {
|
|
27
|
+
const parts = partsPool[level] ??= { starts: [], ends: [], combinators: [] };
|
|
28
|
+
parts.starts.length = parts.ends.length = parts.combinators.length = 0;
|
|
29
|
+
return parts;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function scratchKeys(level) {
|
|
33
|
+
const keys = keysPool[level] ??= { ids: [], classes: [], attrs: [], tag: [], pseudo: [] };
|
|
34
|
+
keys.ids.length = keys.classes.length = keys.attrs.length = keys.tag.length = keys.pseudo.length = 0;
|
|
35
|
+
return keys;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A selector's index keys. `subject` is null when the selector is not
|
|
40
|
+
* understood or its subject has no key (the rule then goes to the universal
|
|
41
|
+
* bucket); `ancestors` lists filter keys: "tag", "#id", ".class", "[attr".
|
|
42
|
+
* @returns {{ subject: { kind: "id" | "class" | "attr" | "tag", name: string }[] | null, ancestors: string[] }}
|
|
43
|
+
*/
|
|
44
|
+
export function selectorKeys(selector) {
|
|
45
|
+
text = selector;
|
|
46
|
+
const parts = selectorParts(0, selector.length, 0);
|
|
47
|
+
if (!parts) return { subject: null, ancestors: [] };
|
|
48
|
+
const last = parts.starts.length - 1;
|
|
49
|
+
const subject = subjectKeys(parts.starts[last], parts.ends[last], 1);
|
|
50
|
+
const ancestors = new Set();
|
|
51
|
+
for (let index = 0; index < last; index++) {
|
|
52
|
+
// Only compounds whose right-hand combinator is descendant or child are
|
|
53
|
+
// ancestors of the subject (a sibling's parent is the subject's parent).
|
|
54
|
+
const combinator = parts.combinators[index];
|
|
55
|
+
if (combinator !== SPACE && combinator !== GREATER) continue;
|
|
56
|
+
const keys = compoundKeys(parts.starts[index], parts.ends[index], 0);
|
|
57
|
+
if (keys.tag.length) ancestors.add(name(keys.tag, true));
|
|
58
|
+
for (let at = 0; at < keys.ids.length; at += 3) ancestors.add(`#${name(keys.ids, false, at)}`);
|
|
59
|
+
for (let at = 0; at < keys.classes.length; at += 3) ancestors.add(`.${name(keys.classes, false, at)}`);
|
|
60
|
+
for (let at = 0; at < keys.attrs.length; at += 3) ancestors.add(`[${name(keys.attrs, true, at)}`);
|
|
61
|
+
}
|
|
62
|
+
return { subject, ancestors: [...ancestors] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Splits [start, end) into compounds and the combinators between them (" ",
|
|
67
|
+
* ">", "+", "~"), ignoring anything inside (), [] and quotes. Returns null
|
|
68
|
+
* for selectors this does not understand. As before this module existed, an
|
|
69
|
+
* escape or a quoted string does not take a pending combinator, so
|
|
70
|
+
* `combinators` may be shorter than `starts` minus one.
|
|
71
|
+
*/
|
|
72
|
+
function selectorParts(start, end, level) {
|
|
73
|
+
const parts = scratchParts(level);
|
|
74
|
+
const { starts, ends, combinators } = parts;
|
|
75
|
+
let current = -1;
|
|
76
|
+
let pending = 0;
|
|
77
|
+
let depth = 0;
|
|
78
|
+
for (let index = start; index < end; index++) {
|
|
79
|
+
const code = text.charCodeAt(index);
|
|
80
|
+
if (code === BACKSLASH) {
|
|
81
|
+
if (current < 0) current = index;
|
|
82
|
+
index++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (code === QUOTE || code === APOSTROPHE) {
|
|
86
|
+
const close = text.indexOf(text[index], index + 1);
|
|
87
|
+
if (close < 0 || close >= end) return null;
|
|
88
|
+
if (current < 0) current = index;
|
|
89
|
+
index = close;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (code === OPEN_PAREN || code === OPEN_BRACKET) depth++;
|
|
93
|
+
else if (code === CLOSE_PAREN || code === CLOSE_BRACKET) depth--;
|
|
94
|
+
if (depth === 0 && (code === SPACE || code === GREATER || code === PLUS || code === TILDE
|
|
95
|
+
|| code === TAB || code === LINE_FEED || code === CARRIAGE_RETURN || code === FORM_FEED)) {
|
|
96
|
+
if (current >= 0) {
|
|
97
|
+
starts.push(current);
|
|
98
|
+
ends.push(index);
|
|
99
|
+
current = -1;
|
|
100
|
+
pending = SPACE;
|
|
101
|
+
}
|
|
102
|
+
if (code === GREATER || code === PLUS || code === TILDE) {
|
|
103
|
+
if (pending === 0 && starts.length === 0) return null;
|
|
104
|
+
pending = code;
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (pending !== 0) {
|
|
109
|
+
combinators.push(pending);
|
|
110
|
+
pending = 0;
|
|
111
|
+
}
|
|
112
|
+
if (current < 0) current = index;
|
|
113
|
+
}
|
|
114
|
+
if (depth !== 0) return null;
|
|
115
|
+
if (current >= 0) {
|
|
116
|
+
starts.push(current);
|
|
117
|
+
ends.push(end);
|
|
118
|
+
} else if (pending !== 0 && starts.length) return null;
|
|
119
|
+
return starts.length ? parts : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Keys of the compound [start, end), from its parts outside (): ids,
|
|
124
|
+
* classes and attribute names as flat [start, end, escaped] triples, the
|
|
125
|
+
* type as one (empty when none), and pseudo-classes as [name start, name
|
|
126
|
+
* end, colon] triples.
|
|
127
|
+
*/
|
|
128
|
+
function compoundKeys(start, end, level) {
|
|
129
|
+
const keys = scratchKeys(level);
|
|
130
|
+
const pipe = text.indexOf("|", start);
|
|
131
|
+
if (pipe !== -1 && pipe < end) return keys;
|
|
132
|
+
if (start < end && isAlpha(text.charCodeAt(start))) {
|
|
133
|
+
let tagEnd = start + 1;
|
|
134
|
+
while (tagEnd < end && isNameCode(text.charCodeAt(tagEnd))) tagEnd++;
|
|
135
|
+
keys.tag.push(start, tagEnd, 0);
|
|
136
|
+
}
|
|
137
|
+
let depth = 0;
|
|
138
|
+
for (let index = start; index < end; index++) {
|
|
139
|
+
const code = text.charCodeAt(index);
|
|
140
|
+
if (code === BACKSLASH) {
|
|
141
|
+
index++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (code === OPEN_PAREN) {
|
|
145
|
+
depth++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (code === CLOSE_PAREN) {
|
|
149
|
+
depth--;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (depth > 0) continue;
|
|
153
|
+
if (code === OPEN_BRACKET) {
|
|
154
|
+
let nameStart = index + 1;
|
|
155
|
+
while (nameStart < end && isSpace(text.charCodeAt(nameStart))) nameStart++;
|
|
156
|
+
let nameEnd = nameStart;
|
|
157
|
+
while (nameEnd < end && isNameCode(text.charCodeAt(nameEnd))) nameEnd++;
|
|
158
|
+
if (nameEnd > nameStart) keys.attrs.push(nameStart, nameEnd, 0);
|
|
159
|
+
// Skip to the closing bracket; quoted values may contain "]", "#" or ".".
|
|
160
|
+
let quote = 0;
|
|
161
|
+
for (index++; index < end; index++) {
|
|
162
|
+
const inner = text.charCodeAt(index);
|
|
163
|
+
if (inner === BACKSLASH) index++;
|
|
164
|
+
else if (quote) {
|
|
165
|
+
if (inner === quote) quote = 0;
|
|
166
|
+
} else if (inner === QUOTE || inner === APOSTROPHE) quote = inner;
|
|
167
|
+
else if (inner === CLOSE_BRACKET) break;
|
|
168
|
+
}
|
|
169
|
+
} else if (code === HASH || code === PERIOD) {
|
|
170
|
+
const identifier = leadingIdentifier(index + 1, end);
|
|
171
|
+
const first = text.charCodeAt(index + 1);
|
|
172
|
+
// A class name cannot start with a digit; an id may.
|
|
173
|
+
if (identifier && !(code === PERIOD && first >= 0x30 && first <= 0x39)) {
|
|
174
|
+
(code === HASH ? keys.ids : keys.classes).push(index + 1, identifier.end, identifier.escaped);
|
|
175
|
+
}
|
|
176
|
+
} else if (code === COLON) {
|
|
177
|
+
let nameStart = index + 1;
|
|
178
|
+
if (nameStart < end && text.charCodeAt(nameStart) === COLON) nameStart++;
|
|
179
|
+
let nameEnd = nameStart;
|
|
180
|
+
while (nameEnd < end && isNameCode(text.charCodeAt(nameEnd))) nameEnd++;
|
|
181
|
+
if (nameEnd > nameStart) keys.pseudo.push(nameStart, nameEnd, index);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return keys;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The identifier at `start` (word characters, "-", non-ASCII, and escapes
|
|
189
|
+
* other than hex ones), or null when absent or followed by a hex escape.
|
|
190
|
+
*/
|
|
191
|
+
function leadingIdentifier(start, end) {
|
|
192
|
+
let index = start;
|
|
193
|
+
let escaped = 0;
|
|
194
|
+
while (index < end) {
|
|
195
|
+
const code = text.charCodeAt(index);
|
|
196
|
+
if (isNameCode(code) || code >= 0x80) index++;
|
|
197
|
+
else if (code === BACKSLASH && index + 1 < end && !isHexDigit(text.charCodeAt(index + 1)) && !isSpace(text.charCodeAt(index + 1))) {
|
|
198
|
+
index += 2;
|
|
199
|
+
escaped = 1;
|
|
200
|
+
} else break;
|
|
201
|
+
}
|
|
202
|
+
if (index === start || (index < end && text.charCodeAt(index) === BACKSLASH)) return null;
|
|
203
|
+
return { end: index, escaped };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Bucket keys for the subject compound: one id, class, attribute or type key,
|
|
208
|
+
* or one per alternative of an :is()/:where() whose alternatives all have one.
|
|
209
|
+
*/
|
|
210
|
+
function subjectKeys(start, end, level) {
|
|
211
|
+
const keys = compoundKeys(start, end, level);
|
|
212
|
+
if (keys.ids.length) return [{ kind: "id", name: name(keys.ids, false) }];
|
|
213
|
+
if (keys.classes.length) return [{ kind: "class", name: name(keys.classes, false) }];
|
|
214
|
+
if (keys.attrs.length) return [{ kind: "attr", name: name(keys.attrs, true) }];
|
|
215
|
+
if (keys.tag.length) return [{ kind: "tag", name: name(keys.tag, true) }];
|
|
216
|
+
const pseudo = keys.pseudo;
|
|
217
|
+
for (let at = 0; at < pseudo.length; at += 3) {
|
|
218
|
+
if (equalsLowerCase(pseudo[at], pseudo[at + 1], "root")) return [{ kind: "tag", name: "html" }];
|
|
219
|
+
}
|
|
220
|
+
for (let at = 0; at < pseudo.length; at += 3) {
|
|
221
|
+
const nameStart = pseudo[at];
|
|
222
|
+
const nameEnd = pseudo[at + 1];
|
|
223
|
+
if (!equalsLowerCase(nameStart, nameEnd, "is") && !equalsLowerCase(nameStart, nameEnd, "where")
|
|
224
|
+
&& !equalsLowerCase(nameStart, nameEnd, "matches") && !equalsLowerCase(nameStart, nameEnd, "-webkit-any")) continue;
|
|
225
|
+
const open = text.indexOf("(", pseudo[at + 2]);
|
|
226
|
+
const close = open < 0 || open >= end ? -1 : matchingParenthesis(open, end);
|
|
227
|
+
if (close < 0) return null;
|
|
228
|
+
const result = [];
|
|
229
|
+
let depth = 0;
|
|
230
|
+
let from = open + 1;
|
|
231
|
+
for (let index = open + 1; index <= close; index++) {
|
|
232
|
+
// The closing parenthesis ends the last alternative.
|
|
233
|
+
const code = index < close ? text.charCodeAt(index) : COMMA;
|
|
234
|
+
if (index < close && code === OPEN_PAREN) depth++;
|
|
235
|
+
else if (index < close && code === CLOSE_PAREN) depth--;
|
|
236
|
+
else if (code === COMMA && (depth === 0 || index === close)) {
|
|
237
|
+
let alternativeStart = from;
|
|
238
|
+
let alternativeEnd = index;
|
|
239
|
+
from = index + 1;
|
|
240
|
+
while (alternativeStart < alternativeEnd && isSpace(text.charCodeAt(alternativeStart))) alternativeStart++;
|
|
241
|
+
while (alternativeEnd > alternativeStart && isSpace(text.charCodeAt(alternativeEnd - 1))) alternativeEnd--;
|
|
242
|
+
if (alternativeStart === alternativeEnd) continue;
|
|
243
|
+
// Only simple alternatives: complex ones inside :is() are left to the
|
|
244
|
+
// selector engine unindexed, so indexing never changes a result.
|
|
245
|
+
const parts = selectorParts(alternativeStart, alternativeEnd, level);
|
|
246
|
+
const inner = parts?.starts.length === 1 ? subjectKeys(parts.starts[0], parts.ends[0], level + 1) : null;
|
|
247
|
+
if (!inner) return null;
|
|
248
|
+
result.push(...inner);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return result.length ? result : null;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function matchingParenthesis(open, end) {
|
|
257
|
+
let depth = 0;
|
|
258
|
+
for (let index = open; index < end; index++) {
|
|
259
|
+
const code = text.charCodeAt(index);
|
|
260
|
+
if (code === BACKSLASH) index++;
|
|
261
|
+
else if (code === OPEN_PAREN) depth++;
|
|
262
|
+
else if (code === CLOSE_PAREN && --depth === 0) return index;
|
|
263
|
+
}
|
|
264
|
+
return -1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The name of a [start, end, escaped] triple at `at`: unescaped, lowercased for types and attributes. */
|
|
268
|
+
function name(triple, lowerCase, at = 0) {
|
|
269
|
+
let value = text.slice(triple[at], triple[at + 1]);
|
|
270
|
+
if (triple[at + 2]) value = value.replace(/\\(.)/g, "$1");
|
|
271
|
+
return lowerCase ? value.toLowerCase() : value;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function equalsLowerCase(start, end, expected) {
|
|
275
|
+
if (end - start !== expected.length) return false;
|
|
276
|
+
for (let index = 0; index < expected.length; index++) {
|
|
277
|
+
let code = text.charCodeAt(start + index);
|
|
278
|
+
if (code >= 0x41 && code <= 0x5a) code += 0x20;
|
|
279
|
+
if (code !== expected.charCodeAt(index)) return false;
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isAlpha(code) {
|
|
285
|
+
return (code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** [\w-]: ASCII letters, digits, "_" and "-". */
|
|
289
|
+
function isNameCode(code) {
|
|
290
|
+
return isAlpha(code) || (code >= 0x30 && code <= 0x39) || code === UNDERSCORE || code === HYPHEN;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isHexDigit(code) {
|
|
294
|
+
return (code >= 0x30 && code <= 0x39) || (code >= 0x61 && code <= 0x66) || (code >= 0x41 && code <= 0x46);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** JavaScript white space: what /\s/ matches and trim() removes. */
|
|
298
|
+
function isSpace(code) {
|
|
299
|
+
return code === SPACE || (code >= TAB && code <= CARRIAGE_RETURN) || code === 0xa0 || code === 0x1680
|
|
300
|
+
|| (code >= 0x2000 && code <= 0x200a) || code === 0x2028 || code === 0x2029 || code === 0x202f
|
|
301
|
+
|| code === 0x205f || code === 0x3000 || code === 0xfeff;
|
|
302
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a style sheet's selectors make sensitive to DOM changes, for the
|
|
3
|
+
* renderer's style invalidation (see selectorTraitsFor in page-renderer.js).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** One sheet's selector traits (sibling combinators, :has(), structural pseudo-classes, invalidation features). */
|
|
7
|
+
export function sheetTraits(sheet) {
|
|
8
|
+
const text = String(sheet).replace(/\/\*[\s\S]*?\*\//g, "");
|
|
9
|
+
const plain = text.replace(/\[[^\]]*\]/g, "[]").replace(/\{[^}]*\}/g, "{}");
|
|
10
|
+
const sibling = /[^\s~|^$*=\[]\s*[+~]\s*[^\s=]/.test(plain);
|
|
11
|
+
const hasCompounds = new Set();
|
|
12
|
+
let hasSiblings = false;
|
|
13
|
+
const selectorsOnly = text.replace(/\{[^}]*\}/g, "{}");
|
|
14
|
+
for (const block of hasBlocks(selectorsOnly)) {
|
|
15
|
+
for (const selector of splitTopLevelText(block.slice(0, -1), ",")) {
|
|
16
|
+
for (const compound of compoundsOf(selector)) {
|
|
17
|
+
if (!compound.includes(":has(")) continue;
|
|
18
|
+
hasCompounds.add(compound.replace(/::[\w-]+(\([^)]*\))?$/, ""));
|
|
19
|
+
if (/:has\(\s*[+~]/.test(compound)) hasSiblings = true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Structural pseudo-classes: which siblings a child list change can
|
|
24
|
+
// affect, and the compounds where they are not the subject (".a:last-child .b"),
|
|
25
|
+
// whose matches decide whether a sibling's subtree must restyle too.
|
|
26
|
+
const structural = /:(?:nth-child|nth-last-child|first-child|last-child|only-child|nth-of-type|nth-last-of-type|first-of-type|last-of-type|only-of-type)\b/;
|
|
27
|
+
const structuralCompounds = new Set();
|
|
28
|
+
for (const block of selectorsOnly.matchAll(/[^{}]*\{/g)) {
|
|
29
|
+
if (!structural.test(block[0])) continue;
|
|
30
|
+
for (const selector of splitTopLevelText(block[0].slice(0, -1), ",")) {
|
|
31
|
+
const compounds = compoundsOf(selector);
|
|
32
|
+
for (const compound of compounds.slice(0, -1)) if (structural.test(compound)) structuralCompounds.add(compound);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Invalidation features (class ".x", id "#x", attribute "[x", tag "t:x",
|
|
36
|
+
// state ":state", anything "*") of compounds that are not a selector's
|
|
37
|
+
// subject: left of a descendant/child combinator, a change to them can
|
|
38
|
+
// restyle the element's subtree; left of + / ~, its later siblings.
|
|
39
|
+
const descendantFeatures = new Set();
|
|
40
|
+
const siblingFeatures = new Set();
|
|
41
|
+
const descendantStateCompounds = [];
|
|
42
|
+
// Selectors ending in a compound right of + / ~ that is not the subject
|
|
43
|
+
// (".a + .b" of ".a + .b .c"): a later sibling's subtree restyles only
|
|
44
|
+
// when its match of one of them changes.
|
|
45
|
+
const siblingTargetCompounds = new Set();
|
|
46
|
+
for (const block of selectorsOnly.matchAll(/[^{}@]*\{/g)) {
|
|
47
|
+
for (const selector of splitTopLevelText(block[0].slice(0, -1), ",")) {
|
|
48
|
+
const parts = complexParts(selector);
|
|
49
|
+
parts.forEach(({ compound, combinator }, index) => {
|
|
50
|
+
const nested = /\([^)]*[\s>+~][^)]*\)/.test(compound); // :is(.a .b) hides combinators
|
|
51
|
+
const previous = parts[index - 1]?.combinator;
|
|
52
|
+
if ((previous === "+" || previous === "~") && index < parts.length - 1) {
|
|
53
|
+
// With what is left of it ("li:first-child + li"): the relation to
|
|
54
|
+
// the earlier sibling can end while the compound still matches.
|
|
55
|
+
const left = parts.slice(0, index).map((part) => `${part.compound} ${part.combinator === " " ? "" : part.combinator + " "}`).join("");
|
|
56
|
+
siblingTargetCompounds.add(left + compound);
|
|
57
|
+
}
|
|
58
|
+
if (index === parts.length - 1 && !nested) return;
|
|
59
|
+
const features = compoundFeatures(compound);
|
|
60
|
+
if (nested || combinator === " " || combinator === ">") {
|
|
61
|
+
for (const feature of features) descendantFeatures.add(feature);
|
|
62
|
+
const states = features.filter((feature) => feature.startsWith(":"));
|
|
63
|
+
if (states.length) descendantStateCompounds.push({ states, required: positiveCompoundFeatures(compound) });
|
|
64
|
+
}
|
|
65
|
+
if (nested || combinator === "+" || combinator === "~") for (const feature of features) siblingFeatures.add(feature);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
sibling, hasCompounds, hasSiblings, structuralCompounds, descendantFeatures, siblingFeatures, descendantStateCompounds, siblingTargetCompounds,
|
|
71
|
+
forward: /:(?:nth-child|nth-of-type|first-of-type|only-of-type)\b/.test(selectorsOnly),
|
|
72
|
+
firstChild: /:(?:first-child|only-child)\b/.test(selectorsOnly),
|
|
73
|
+
backward: /:(?:nth-last-child|nth-last-of-type|last-of-type|only-of-type)\b/.test(selectorsOnly),
|
|
74
|
+
lastChild: /:(?:last-child|only-child)\b/.test(selectorsOnly),
|
|
75
|
+
defined: text.includes(":defined"), target: /:target\b/.test(text),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const HAS_BLOCK = /[^{}]*:has\([^{]*\{/y;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The matches of /[^{}]*:has\([^{]*\{/g in `text`. Searched globally, that
|
|
83
|
+
* expression retries at every position of every brace-free run; a match can
|
|
84
|
+
* only start where such a run starts (after a brace, where the previous
|
|
85
|
+
* match ended) and contains ":has(", so only those runs are tried.
|
|
86
|
+
*/
|
|
87
|
+
function* hasBlocks(text) {
|
|
88
|
+
let end = 0;
|
|
89
|
+
for (let at = text.indexOf(":has("); at !== -1; at = text.indexOf(":has(", Math.max(at + 1, end))) {
|
|
90
|
+
const start = Math.max(text.lastIndexOf("{", at), text.lastIndexOf("}", at)) + 1;
|
|
91
|
+
if (start < end) continue;
|
|
92
|
+
HAS_BLOCK.lastIndex = start;
|
|
93
|
+
const match = HAS_BLOCK.exec(text);
|
|
94
|
+
if (!match) continue;
|
|
95
|
+
end = HAS_BLOCK.lastIndex;
|
|
96
|
+
yield match[0];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Splits at top-level `separator` (outside (), [] and quotes). */
|
|
101
|
+
function splitTopLevelText(text, separator) {
|
|
102
|
+
const parts = [];
|
|
103
|
+
let depth = 0;
|
|
104
|
+
let quote = "";
|
|
105
|
+
let start = 0;
|
|
106
|
+
for (let index = 0; index < text.length; index++) {
|
|
107
|
+
const character = text[index];
|
|
108
|
+
if (quote) { if (character === "\\") index++; else if (character === quote) quote = ""; continue; }
|
|
109
|
+
if (character === '"' || character === "'") quote = character;
|
|
110
|
+
else if (character === "\\") index++;
|
|
111
|
+
else if (character === "(" || character === "[") depth++;
|
|
112
|
+
else if (character === ")" || character === "]") depth--;
|
|
113
|
+
else if (character === separator && depth === 0) { parts.push(text.slice(start, index).trim()); start = index + 1; }
|
|
114
|
+
}
|
|
115
|
+
parts.push(text.slice(start).trim());
|
|
116
|
+
return parts.filter(Boolean);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const STATE_PSEUDO = /:(?:checked|indeterminate|default|disabled|enabled|placeholder-shown|valid|invalid|in-range|out-of-range|required|optional|read-only|read-write|user-valid|user-invalid|autofill|open|popover-open|modal|target|focus|focus-visible|focus-within|hover|active|visited|link|any-link|defined|fullscreen)\b/g;
|
|
120
|
+
|
|
121
|
+
/** CSS escapes in an identifier (".\\:popover-open", "\\31 a") resolved. */
|
|
122
|
+
function cssUnescape(text) {
|
|
123
|
+
return text.replace(/\\([0-9a-fA-F]{1,6})\s?/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16))).replace(/\\(.)/g, "$1");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Invalidation features a compound selector mentions (see selectorTraitsFor). */
|
|
127
|
+
function compoundFeatures(compound) {
|
|
128
|
+
const features = [];
|
|
129
|
+
const plain = compound.replace(/\[[^\]]*\]/g, (attribute) => {
|
|
130
|
+
const name = /^\[\s*([\w-]+)/.exec(attribute)?.[1];
|
|
131
|
+
if (name) features.push("[" + name.toLowerCase());
|
|
132
|
+
return "";
|
|
133
|
+
});
|
|
134
|
+
for (const match of plain.matchAll(/([.#])((?:\\[0-9a-fA-F]{1,6}\s?|\\.|[\w\u00a0-\uffff-])+)/g)) features.push(match[1] + cssUnescape(match[2]));
|
|
135
|
+
const tag = /^(?:[a-zA-Z][\w-]*)/.exec(plain)?.[0];
|
|
136
|
+
if (tag) features.push("t:" + tag.toLowerCase());
|
|
137
|
+
for (const match of plain.matchAll(STATE_PSEUDO)) features.push(match[0]);
|
|
138
|
+
// Nothing but a universal selector or pseudo-classes: it can match anything.
|
|
139
|
+
if (!features.length || /^\*/.test(plain)) features.push("*");
|
|
140
|
+
return features;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Positive outer-compound requirements that must hold regardless of pseudo state. */
|
|
144
|
+
function positiveCompoundFeatures(compound) {
|
|
145
|
+
const features = [];
|
|
146
|
+
const tag = /^[a-zA-Z][\w-]*/.exec(compound)?.[0];
|
|
147
|
+
if (tag) features.push("t:" + tag.toLowerCase());
|
|
148
|
+
let depth = 0;
|
|
149
|
+
for (let index = 0; index < compound.length; index++) {
|
|
150
|
+
const character = compound[index];
|
|
151
|
+
if (character === "\\") { index++; continue; }
|
|
152
|
+
if (character === "(") { depth++; continue; }
|
|
153
|
+
if (character === ")") { depth--; continue; }
|
|
154
|
+
if (depth) continue;
|
|
155
|
+
if (character === "[") {
|
|
156
|
+
const name = /^\s*([\w-]+)/.exec(compound.slice(index + 1))?.[1];
|
|
157
|
+
if (name) features.push("[" + name.toLowerCase());
|
|
158
|
+
let quote = "";
|
|
159
|
+
for (index++; index < compound.length; index++) {
|
|
160
|
+
const inner = compound[index];
|
|
161
|
+
if (inner === "\\") index++;
|
|
162
|
+
else if (quote) { if (inner === quote) quote = ""; }
|
|
163
|
+
else if (inner === '"' || inner === "'") quote = inner;
|
|
164
|
+
else if (inner === "]") break;
|
|
165
|
+
}
|
|
166
|
+
} else if (character === "." || character === "#") {
|
|
167
|
+
const match = /^((?:\\[0-9a-fA-F]{1,6}\s?|\\.|[\w\u00a0-\uffff-])+)/.exec(compound.slice(index + 1));
|
|
168
|
+
if (match) features.push(character + cssUnescape(match[1]));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return features;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The features an element has (for a sibling it inserts next to, for example). */
|
|
175
|
+
export function elementFeatures(element) {
|
|
176
|
+
const features = ["t:" + element.localName];
|
|
177
|
+
const id = element.getAttribute("id");
|
|
178
|
+
if (id) features.push("#" + id);
|
|
179
|
+
for (const name of element.classList) features.push("." + name);
|
|
180
|
+
for (const attribute of element.attributes) features.push("[" + attribute.name);
|
|
181
|
+
return features;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Whether a character code is JavaScript white space (what /\s/ matches and trim() removes). */
|
|
185
|
+
function isSpace(code) {
|
|
186
|
+
return code === 0x20 || (code >= 0x09 && code <= 0x0d) || code === 0xa0 || code === 0x1680
|
|
187
|
+
|| (code >= 0x2000 && code <= 0x200a) || code === 0x2028 || code === 0x2029 || code === 0x202f
|
|
188
|
+
|| code === 0x205f || code === 0x3000 || code === 0xfeff;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const GREATER = 0x3e, PLUS = 0x2b, TILDE = 0x7e;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Calls `separator(index, code)` at each top-level combinator character of
|
|
195
|
+
* a complex selector (white space, ">", "+", "~" outside (), [], quotes and
|
|
196
|
+
* escapes) and returns nothing; the text between separators is a compound.
|
|
197
|
+
*/
|
|
198
|
+
function forEachSeparator(selector, separator) {
|
|
199
|
+
let depth = 0;
|
|
200
|
+
let quote = 0;
|
|
201
|
+
for (let index = 0; index < selector.length; index++) {
|
|
202
|
+
const code = selector.charCodeAt(index);
|
|
203
|
+
if (quote) {
|
|
204
|
+
if (code === 0x5c) index++;
|
|
205
|
+
else if (code === quote) quote = 0;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (code === 0x22 || code === 0x27) { quote = code; continue; }
|
|
209
|
+
if (code === 0x5c) { index++; continue; }
|
|
210
|
+
if (code === 0x28 || code === 0x5b) depth++;
|
|
211
|
+
else if (code === 0x29 || code === 0x5d) depth--;
|
|
212
|
+
else if (depth === 0 && (code === GREATER || code === PLUS || code === TILDE || isSpace(code))) separator(index, code);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** A complex selector's compounds with the combinator after each (" ", ">", "+", "~", or null for the subject). */
|
|
217
|
+
function complexParts(selector) {
|
|
218
|
+
const parts = [];
|
|
219
|
+
let start = 0;
|
|
220
|
+
forEachSeparator(selector, (index, code) => {
|
|
221
|
+
if (index > start) parts.push({ compound: selector.slice(start, index), combinator: null });
|
|
222
|
+
start = index + 1;
|
|
223
|
+
if (!isSpace(code) && parts.length) parts.at(-1).combinator = selector[index];
|
|
224
|
+
else if (parts.length && parts.at(-1).combinator === null) parts.at(-1).combinator = " ";
|
|
225
|
+
});
|
|
226
|
+
if (selector.length > start) parts.push({ compound: selector.slice(start), combinator: null });
|
|
227
|
+
return parts;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The compound selectors of a complex selector (split at top-level combinators). */
|
|
231
|
+
function compoundsOf(selector) {
|
|
232
|
+
const compounds = [];
|
|
233
|
+
let start = 0;
|
|
234
|
+
forEachSeparator(selector, (index) => {
|
|
235
|
+
if (index > start) compounds.push(selector.slice(start, index));
|
|
236
|
+
start = index + 1;
|
|
237
|
+
});
|
|
238
|
+
if (selector.length > start) compounds.push(selector.slice(start));
|
|
239
|
+
return compounds;
|
|
240
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Chrome 154 stable for desktop (2026-09-22). Keep CDP discovery, page JS, and
|
|
2
|
+
// HTTP requests on the same advertised version; casty reads /json/version.
|
|
3
|
+
export const CHROMIUM_VERSION = "154.0.8037.57";
|
|
4
|
+
|
|
5
|
+
export function desktopUserAgent(platform = process.platform, arch = process.arch) {
|
|
6
|
+
const system = platform === "darwin" ? "Macintosh; Intel Mac OS X 10_15_7"
|
|
7
|
+
: platform === "win32" ? "Windows NT 10.0; Win64; x64"
|
|
8
|
+
: `X11; Linux ${arch === "arm64" ? "aarch64" : "x86_64"}`;
|
|
9
|
+
return `Mozilla/5.0 (${system}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const DESKTOP_USER_AGENT = desktopUserAgent();
|
package/package.json
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts
|
|
2
|
+
index f439646..ce2dbe5 100644
|
|
3
|
+
--- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts
|
|
4
|
+
+++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts
|
|
5
|
+
@@ -566,16 +566,7 @@ export default class HTMLScriptElement extends HTMLElement {
|
|
6
|
+
browserSettings &&
|
|
7
|
+
(browserSettings.disableJavaScriptFileLoading || !browserSettings.enableJavaScriptEvaluation)
|
|
8
|
+
) {
|
|
9
|
+
- if (browserSettings.handleDisabledFileLoadingAsSuccess) {
|
|
10
|
+
- this.dispatchEvent(new Event('load'));
|
|
11
|
+
- } else {
|
|
12
|
+
- const error = new window.DOMException(
|
|
13
|
+
- `Failed to load module "${url}". JavaScript file loading is disabled.`,
|
|
14
|
+
- DOMExceptionNameEnum.notSupportedError
|
|
15
|
+
- );
|
|
16
|
+
- browserFrame.page.console.error(error);
|
|
17
|
+
- this.dispatchEvent(new Event('error'));
|
|
18
|
+
- }
|
|
19
|
+
+ // The renderer owns script fetching and dispatches load/error after execution.
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@@ -647,16 +638,7 @@ export default class HTMLScriptElement extends HTMLElement {
|
|
24
|
+
browserSettings &&
|
|
25
|
+
(browserSettings.disableJavaScriptFileLoading || !browserSettings.enableJavaScriptEvaluation)
|
|
26
|
+
) {
|
|
27
|
+
- if (browserSettings.handleDisabledFileLoadingAsSuccess) {
|
|
28
|
+
- this.dispatchEvent(new Event('load'));
|
|
29
|
+
- } else {
|
|
30
|
+
- const error = new window.DOMException(
|
|
31
|
+
- `Failed to load script "${absoluteURL}". JavaScript file loading is disabled.`,
|
|
32
|
+
- DOMExceptionNameEnum.notSupportedError
|
|
33
|
+
- );
|
|
34
|
+
- browserFrame.page.console.error(error);
|
|
35
|
+
- this.dispatchEvent(new Event('error'));
|
|
36
|
+
- }
|
|
37
|
+
+ // The renderer owns script fetching and dispatches load/error after execution.
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts
|
|
2
|
+
--- a/packages/happy-dom/src/nodes/element/Element.ts
|
|
3
|
+
+++ b/packages/happy-dom/src/nodes/element/Element.ts
|
|
4
|
+
@@ -706,12 +706,9 @@
|
|
5
|
+
* @param text HTML string to insert.
|
|
6
|
+
*/
|
|
7
|
+
public insertAdjacentHTML(position: InsertAdjacentPosition, text: string): void {
|
|
8
|
+
- const childNodes = new HTMLParser(this[PropertySymbol.window]).parse(text)[
|
|
9
|
+
- PropertySymbol.nodeArray
|
|
10
|
+
- ];
|
|
11
|
+
- while (childNodes.length) {
|
|
12
|
+
- this.insertAdjacentElement(position, childNodes[0]);
|
|
13
|
+
- }
|
|
14
|
+
+ // Insert the parsed fragment as a whole: inserting its nodes one at a
|
|
15
|
+
+ // time at "afterbegin" or "afterend" reversed their order.
|
|
16
|
+
+ this.insertAdjacentElement(position, new HTMLParser(this[PropertySymbol.window]).parse(text));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts
|
|
2
|
+
--- a/packages/happy-dom/src/query-selector/QuerySelector.ts
|
|
3
|
+
+++ b/packages/happy-dom/src/query-selector/QuerySelector.ts
|
|
4
|
+
@@ -373,7 +373,10 @@ export default class QuerySelector {
|
|
5
|
+
);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
- const cachedResult = element[PropertySymbol.cache].matches.get(selector);
|
|
9
|
+
+ // A :has() match depends on the whole subtree, which no affected-cache
|
|
10
|
+
+ // registration covers, so it is not cached.
|
|
11
|
+
+ const cacheable = !selector.includes(':has(');
|
|
12
|
+
+ const cachedResult = cacheable ? element[PropertySymbol.cache].matches.get(selector) : null;
|
|
13
|
+
|
|
14
|
+
if (cachedResult) {
|
|
15
|
+
if (cachedResult.result !== null) {
|
|
16
|
+
@@ -386,9 +389,11 @@ export default class QuerySelector {
|
|
17
|
+
result: { match: null }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
- element[PropertySymbol.cache].matches.set(selector, cachedItem);
|
|
21
|
+
+ if (cacheable) {
|
|
22
|
+
+ element[PropertySymbol.cache].matches.set(selector, cachedItem);
|
|
23
|
+
+ }
|
|
24
|
+
|
|
25
|
+
- if (element[PropertySymbol.isConnected]) {
|
|
26
|
+
+ if (cacheable && element[PropertySymbol.isConnected]) {
|
|
27
|
+
// Document is affected for the ":target" selector
|
|
28
|
+
(element[PropertySymbol.ownerDocument] || element)[PropertySymbol.affectsCache].push(
|
|
29
|
+
cachedItem
|
|
30
|
+
@@ -462,6 +467,13 @@ export default class QuerySelector {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
+ // Structural pseudo-classes and sibling combinators read the parent's
|
|
35
|
+
+ // child list, whose changes are reported on the parent.
|
|
36
|
+
+ const parent = element.parentNode;
|
|
37
|
+
+ if (parent) {
|
|
38
|
+
+ this.affectsCache(<Element>parent, cachedItem);
|
|
39
|
+
+ }
|
|
40
|
+
+
|
|
41
|
+
const result = selectorItem.match(scope, element, ignoreErrors);
|
|
42
|
+
|
|
43
|
+
if (result) {
|