buninu-browser 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +74 -9
  3. package/ai-debug.log +5537 -0
  4. package/bunfig.toml +1 -0
  5. package/buninu-browser.js +4 -0
  6. package/index.js +5 -0
  7. package/lib/audio/piano.js +24 -1
  8. package/lib/audio/playback-buffer.js +27 -0
  9. package/lib/audio/pulse-client.js +137 -19
  10. package/lib/cdp-server.js +22 -11
  11. package/lib/happy-dom/parser.js +29 -4
  12. package/lib/happy-dom/selector-pseudo-adapter.js +14 -0
  13. package/lib/headless-shell.js +69 -19
  14. package/lib/input/hit-test.js +25 -7
  15. package/lib/layout/text-layout.js +268 -85
  16. package/lib/log.js +5 -2
  17. package/lib/media/byte-pipe.js +82 -0
  18. package/lib/media/position-audio.js +42 -0
  19. package/lib/media/video-keyframe.js +29 -0
  20. package/lib/network/network-service.js +161 -14
  21. package/lib/paint/display-list.js +316 -15
  22. package/lib/paint/gradient.js +11 -5
  23. package/lib/render-tree/index.js +19 -2
  24. package/lib/render-tree/svg.js +2 -1
  25. package/lib/renderer/bindings.js +995 -69
  26. package/lib/renderer/canvas-2d.js +92 -0
  27. package/lib/renderer/compositor.js +16 -1
  28. package/lib/renderer/custom-element-reactions.js +18 -0
  29. package/lib/renderer/history.js +42 -0
  30. package/lib/renderer/host.js +166 -9
  31. package/lib/renderer/image-loading.js +27 -0
  32. package/lib/renderer/interfaces.js +215 -0
  33. package/lib/renderer/keyboard-input.js +12 -0
  34. package/lib/renderer/main.js +162 -12
  35. package/lib/renderer/media-playback-events.js +13 -0
  36. package/lib/renderer/media-source.js +819 -0
  37. package/lib/renderer/messaging.js +411 -0
  38. package/lib/renderer/native-functions.js +113 -0
  39. package/lib/renderer/page-realm.js +1408 -242
  40. package/lib/renderer/page-renderer.js +1893 -223
  41. package/lib/renderer/piano-input.js +22 -0
  42. package/lib/renderer/pointer-move.js +29 -0
  43. package/lib/renderer/rejections.js +31 -0
  44. package/lib/renderer/rewrite-worker.js +22 -0
  45. package/lib/renderer/script-rewrite.js +805 -57
  46. package/lib/renderer/trusted-types.js +129 -0
  47. package/lib/renderer/video-presentation.js +126 -0
  48. package/lib/renderer/workers.js +327 -0
  49. package/lib/screenshot-cli.js +2 -0
  50. package/lib/style/computed-style.js +186 -29
  51. package/lib/user-agent.js +14 -2
  52. package/package.json +4 -1
  53. package/patches/happy-dom/0002-use-bun-platform-adapters.patch +139 -19
  54. package/patches/happy-dom/0006-parse-css-escapes-in-selectors.patch +115 -0
  55. package/patches/happy-dom/README.md +26 -0
  56. package/patches/ses/0002-embedder-native-functions-source-text-sloppy-code.patch +106 -0
  57. package/patches/ses/0003-cache-sloppy-evaluators.patch +47 -0
  58. package/patches/ses/README.md +4 -0
  59. package/test/canvas-2d.test.js +55 -0
  60. package/test/compositor.test.js +41 -1
  61. package/test/computed-style.test.js +59 -1
  62. package/test/display-list.test.js +88 -1
  63. package/test/download.test.js +43 -0
  64. package/test/fixtures/long-video.mp4 +0 -0
  65. package/test/fixtures/media-sync.js +57 -0
  66. package/test/happy-dom-platform-adapters.test.js +25 -0
  67. package/test/headless-shell.test.js +23 -1
  68. package/test/hit-test.test.js +25 -1
  69. package/test/media-playback-events.test.js +22 -0
  70. package/test/media-source.test.js +156 -0
  71. package/test/media-sync.test.js +12 -0
  72. package/test/mobile.test.js +2 -2
  73. package/test/navigation.test.js +61 -0
  74. package/test/network-service.test.js +14 -0
  75. package/test/page-scripts.test.js +1203 -7
  76. package/test/piano-input.test.js +44 -0
  77. package/test/playback-buffer.test.js +31 -0
  78. package/test/position-audio.test.js +67 -0
  79. package/test/positioning.test.js +95 -1
  80. package/test/pulse-client.test.js +84 -0
  81. package/test/renderer-host.test.js +5 -2
  82. package/test/screenshot-cli.test.js +25 -1
  83. package/test/script-rewrite.test.js +111 -6
  84. package/test/setup.js +5 -0
  85. package/test/text-layout.test.js +62 -0
  86. package/test/user-agent.test.js +60 -4
  87. package/test/video-presentation.test.js +67 -0
  88. package/vendor/happy-dom/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts +52 -22
  89. package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +18 -17
  90. package/vendor/ses/dist/ses.mjs +52 -6
@@ -2,7 +2,16 @@ import { expect, test } from "bun:test";
2
2
  import { CdpServer } from "../lib/cdp-server.js";
3
3
  import { createBrowser } from "../lib/headless-shell.js";
4
4
  import { MOBILE_USER_AGENT } from "../lib/renderer/flags.js";
5
- import { CHROMIUM_VERSION, DESKTOP_USER_AGENT, desktopUserAgent } from "../lib/user-agent.js";
5
+ import { BROWSER_PRODUCT, CHROMIUM_VERSION, DESKTOP_USER_AGENT, WEBKIT_USER_AGENT, desktopUserAgent, webkitFromArgs } from "../lib/user-agent.js";
6
+
7
+ test("CLI identity defaults to WebKit, last desktop flag wins and mobile takes priority", () => {
8
+ for (const [args, expected] of [
9
+ [[], true], [["--webkit"], true], [["--chromium"], false],
10
+ [["--webkit", "--chromium"], false], [["--chromium", "--webkit"], true],
11
+ [["--mobile"], false], [["--mobile", "--webkit"], false],
12
+ [["--webkit", "--mobile"], false], [["--mobile", "--chromium"], false],
13
+ ]) expect(webkitFromArgs(args)).toBe(expected);
14
+ });
6
15
 
7
16
  test("desktop user agent uses the same version on each platform", () => {
8
17
  for (const [platform, arch, system] of [
@@ -19,17 +28,17 @@ test("mobile uses current Chrome for Android rather than WebView", () => {
19
28
  expect(MOBILE_USER_AGENT).toBe(`Mozilla/5.0 (Linux; Android 16; ASUSAI2501C Build/BQ2A.250525.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Mobile Safari/537.36`);
20
29
  });
21
30
 
22
- test("CDP discovery, HTTP requests, and navigator report the same desktop user agent", async () => {
31
+ test("Chromium opt-out reports the same identity through CDP, HTTP and navigator", async () => {
23
32
  let requestUserAgent;
24
33
  const site = Bun.serve({ port: 0, fetch(request) {
25
34
  requestUserAgent = request.headers.get("user-agent");
26
35
  return new Response("<!doctype html><title>UA test</title>", { headers: { "content-type": "text/html" } });
27
36
  } });
28
- const browser = await createBrowser({ spareRenderer: false });
37
+ const browser = await createBrowser({ spareRenderer: false, webkit: false });
29
38
  const cdp = CdpServer.create(browser.context).listen(0, "127.0.0.1");
30
39
  try {
31
40
  const version = await fetch(`http://127.0.0.1:${cdp.port}/json/version`).then((response) => response.json());
32
- expect(version.Browser).toBe(`Chrome/${CHROMIUM_VERSION}`);
41
+ expect(version.Browser).toBe(BROWSER_PRODUCT);
33
42
  expect(version["User-Agent"]).toBe(DESKTOP_USER_AGENT);
34
43
  await browser.context.navigate(`http://127.0.0.1:${site.port}/`);
35
44
  expect(requestUserAgent).toBe(DESKTOP_USER_AGENT);
@@ -40,3 +49,50 @@ test("CDP discovery, HTTP requests, and navigator report the same desktop user a
40
49
  site.stop(true);
41
50
  }
42
51
  });
52
+
53
+ test.each([{}, { webkit: true }])("default and explicit WebKit identity is consistent across CDP, requests and navigator: %j", async (options) => {
54
+ let headers;
55
+ const site = Bun.serve({ port: 0, fetch(request) {
56
+ headers = request.headers;
57
+ return new Response("<!doctype html><title>identity</title>", { headers: { "content-type": "text/html" } });
58
+ } });
59
+ const browser = await createBrowser({ spareRenderer: false, ...options });
60
+ const cdp = CdpServer.create(browser.context).listen(0, "127.0.0.1");
61
+ try {
62
+ // Default and explicit WebKit profiles both resist a CDP client's Chrome UA.
63
+ await browser.context.setUserAgent(DESKTOP_USER_AGENT);
64
+ const version = await fetch(`http://127.0.0.1:${cdp.port}/json/version`).then(r => r.json());
65
+ expect(version.Browser).toBe(BROWSER_PRODUCT);
66
+ expect(version["User-Agent"]).toBe(WEBKIT_USER_AGENT);
67
+ await browser.context.navigate(`http://127.0.0.1:${site.port}/`);
68
+ expect(headers.get("user-agent")).toBe(WEBKIT_USER_AGENT);
69
+ expect(headers.has("sec-ch-ua")).toBe(false);
70
+ expect(await browser.context.evaluate(`[
71
+ navigator.userAgent, navigator.vendor, navigator.platform,
72
+ 'userAgentData' in navigator, typeof NavigatorUAData, typeof window.chrome,
73
+ ]`)).toEqual([WEBKIT_USER_AGENT, "Apple Computer, Inc.", "MacIntel", false, "undefined", "undefined"]);
74
+ } finally {
75
+ cdp.stop(true);
76
+ browser.close();
77
+ site.stop(true);
78
+ }
79
+ });
80
+
81
+ test("scripts added for new documents run before the page's own scripts, in every later document", async () => {
82
+ const site = Bun.serve({ port: 0, fetch: () => new Response(
83
+ "<!doctype html><script>window.seenByPage = window.early;</script>",
84
+ { headers: { "content-type": "text/html" } },
85
+ ) });
86
+ const browser = await createBrowser({ spareRenderer: false });
87
+ try {
88
+ const first = browser.context.addInitScript("window.early = (window.early ?? 0) + 1;");
89
+ await browser.context.navigate(`http://127.0.0.1:${site.port}/`);
90
+ expect(await browser.context.evaluate("window.seenByPage")).toBe(1);
91
+ browser.context.removeInitScript(first);
92
+ await browser.context.navigate(`http://127.0.0.1:${site.port}/again`);
93
+ expect(await browser.context.evaluate("window.seenByPage")).toBe(undefined);
94
+ } finally {
95
+ browser.close();
96
+ site.stop(true);
97
+ }
98
+ });
@@ -0,0 +1,67 @@
1
+ import { test, expect } from "bun:test";
2
+ import { VideoPresentation, compositeVideo } from "../lib/renderer/video-presentation.js";
3
+
4
+ test("video predecode is bounded and waits for measured audio, skipping stale frames", async () => {
5
+ let frames = 0, closed = 0;
6
+ const reports = [];
7
+ const engine = { decode: async () => ({ width: 2, height: 2, fps: 30,
8
+ nextFrame: () => { frames++; return new Uint8Array(16).fill(frames); }, close: () => closed++ }) };
9
+ const video = new VideoPresentation(engine, () => {}, r => reports.push(r));
10
+ try {
11
+ await video.open({ id: 1, time: 0, hasAudio: true, audioId: 4, audioStart: 0 });
12
+ await Bun.sleep(100);
13
+ expect(frames).toBeLessThanOrEqual(24);
14
+ expect(video.queue.length).toBeLessThanOrEqual(video.limit);
15
+ expect(video.frame.time).toBe(0);
16
+ const first = reports.length;
17
+ await Bun.sleep(80);
18
+ expect(reports).toHaveLength(first);
19
+ video.timing(4, 0.5);
20
+ await Bun.sleep(60);
21
+ expect(video.frame.time).toBeCloseTo(0.5);
22
+ expect(reports.filter(r => r.time > 0).length).toBe(1);
23
+ video.stop(1);
24
+ video.timing(4, 2);
25
+ await Bun.sleep(30);
26
+ expect(video.frame).toBeNull();
27
+ expect(closed).toBe(1);
28
+ } finally { video.stop(); }
29
+ });
30
+
31
+ test("a late decoder open cannot replace a newer playback generation", async () => {
32
+ let resolve, closed = 0;
33
+ const video = new VideoPresentation({ decode: () => new Promise(r => resolve = r) }, () => {}, () => {});
34
+ const opening = video.open({ id: 1, time: 0 });
35
+ video.stop();
36
+ resolve({ close: () => closed++ });
37
+ await opening;
38
+ expect(closed).toBe(1);
39
+ expect(video.config).toBeNull();
40
+ });
41
+
42
+ test("reopening at a keyframe does not show the prefix before the resume position", async () => {
43
+ let at = 0;
44
+ const reports = [];
45
+ const video = new VideoPresentation({ decode: async () => ({ width: 2, height: 2, fps: 10,
46
+ seek() { at = 0; }, nextFrame(w, h) { at++; return new Uint8Array(w * h * 4).fill(at); }, close() {} }) },
47
+ () => {}, report => reports.push(report));
48
+ try {
49
+ video.timing(9, 2);
50
+ await video.open({ id: 2, time: 2, hasAudio: true, audioId: 9, audioStart: 0 });
51
+ await Bun.sleep(100);
52
+ expect(reports.some(report => report.time >= 1.9)).toBe(true);
53
+ expect(reports.every(report => report.time === undefined || report.time >= 1.9)).toBe(true);
54
+ } finally { video.stop(); }
55
+ });
56
+
57
+ test("video layer preserves opaque controls, translucent overlays and clipped pixels", () => {
58
+ const base = new Uint8Array([0,0,0,255, 255,255,255,255, 0,0,128,255, 10,20,30,255]);
59
+ const mask = new Uint8Array([255,255,255,0, 0,0,0,0, 127,127,127,0, 0,0,0,0]);
60
+ const tile = { pixelWidth: 4, pixelHeight: 1, pixels: base,
61
+ videoLayer: { base, mask, rect: { x: 0, y: 0, width: 4, height: 1 } } };
62
+ const frame = { width: 1, height: 1, pixels: new Uint8Array([200,100,0,255]) };
63
+ expect([...compositeVideo(tile, frame).pixels]).toEqual([
64
+ 200,100,0,255, 255,255,255,255, 100,50,128,255, 10,20,30,255,
65
+ ]);
66
+ expect(compositeVideo(tile, null)).toBe(tile);
67
+ });
@@ -19,6 +19,8 @@ const DATA_URL_REGEX = /^\s*data:([^,]+)?,(.*)/;
19
19
  export default class HTMLImageElement extends HTMLElement implements ICanvasShape {
20
20
  public [PropertySymbol.tagName] = 'IMG';
21
21
  public [PropertySymbol.complete] = true;
22
+ // Bumped whenever src changes: an image decoded for an earlier src is dropped.
23
+ #loadGeneration = 0;
22
24
  public [PropertySymbol.naturalHeight] = 0;
23
25
  public [PropertySymbol.naturalWidth] = 0;
24
26
  public [PropertySymbol.loading] = 'auto';
@@ -357,6 +359,7 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
357
359
  super[PropertySymbol.onSetAttribute](attribute, replacedAttribute);
358
360
 
359
361
  if (attribute[PropertySymbol.name] === 'src') {
362
+ const generation = ++this.#loadGeneration;
360
363
  const url = this.#parseUrl(attribute[PropertySymbol.value]);
361
364
 
362
365
  if (!url) {
@@ -367,13 +370,13 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
367
370
  return;
368
371
  }
369
372
 
370
- if (this.#loadDataUrl(url)) {
373
+ if (this.#loadDataUrl(url, generation)) {
371
374
  return;
372
375
  }
373
376
 
374
377
  const settings = new WindowBrowserContext(this[PropertySymbol.window]).getSettings();
375
378
  if (settings?.enableImageFileLoading) {
376
- this.#loadUrlSource(this.src);
379
+ this.#loadUrlSource(this.src, generation);
377
380
  }
378
381
  }
379
382
  }
@@ -385,6 +388,7 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
385
388
  super[PropertySymbol.onRemoveAttribute](removedAttribute);
386
389
 
387
390
  if (removedAttribute[PropertySymbol.name] === 'src') {
391
+ this.#loadGeneration++;
388
392
  this[PropertySymbol.complete] = true;
389
393
  this[PropertySymbol.naturalHeight] = 0;
390
394
  this[PropertySymbol.naturalWidth] = 0;
@@ -415,27 +419,37 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
415
419
  * Loads the image from the given data URL.
416
420
  *
417
421
  * @param src Source.
422
+ * @param generation The load this is (see #loadGeneration).
418
423
  * @returns True if the image was loaded, false otherwise.
419
424
  */
420
- #loadDataUrl(src: string): boolean {
425
+ #loadDataUrl(src: string, generation: number): boolean {
421
426
  const dataUrlMatch = src.match(DATA_URL_REGEX);
422
427
 
423
428
  if (dataUrlMatch) {
424
429
  const buffer = Buffer.from(dataUrlMatch[2], 'base64');
425
- this[PropertySymbol.complete] = true;
426
430
  this[PropertySymbol.buffer] = buffer;
427
431
  this[PropertySymbol.complete] = false;
428
- ImageSize(buffer).then((dimensions) => {
429
- this[PropertySymbol.naturalHeight] = dimensions.height;
430
- this[PropertySymbol.naturalWidth] = dimensions.width;
431
- this[PropertySymbol.complete] = true;
432
- this.dispatchEvent(new this[PropertySymbol.window].Event('load'));
433
- }).catch(() => {
434
- this[PropertySymbol.naturalHeight] = 0;
435
- this[PropertySymbol.naturalWidth] = 0;
436
- this[PropertySymbol.complete] = true;
437
- this.dispatchEvent(new this[PropertySymbol.window].Event('error'));
438
- });
432
+ // The size is read asynchronously; a src set meanwhile wins.
433
+ ImageSize(buffer).then(
434
+ (dimensions) => {
435
+ if (generation !== this.#loadGeneration) {
436
+ return;
437
+ }
438
+ this[PropertySymbol.naturalHeight] = dimensions.height;
439
+ this[PropertySymbol.naturalWidth] = dimensions.width;
440
+ this[PropertySymbol.complete] = true;
441
+ this.dispatchEvent(new this[PropertySymbol.window].Event('load'));
442
+ },
443
+ () => {
444
+ if (generation !== this.#loadGeneration) {
445
+ return;
446
+ }
447
+ this[PropertySymbol.naturalHeight] = 0;
448
+ this[PropertySymbol.naturalWidth] = 0;
449
+ this[PropertySymbol.complete] = true;
450
+ this.dispatchEvent(new this[PropertySymbol.window].Event('error'));
451
+ }
452
+ );
439
453
  return true;
440
454
  }
441
455
  return false;
@@ -445,8 +459,9 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
445
459
  * Loads the image from the given source.
446
460
  *
447
461
  * @param src Source.
462
+ * @param generation The load this is (see #loadGeneration).
448
463
  */
449
- async #loadUrlSource(src: string): Promise<void> {
464
+ async #loadUrlSource(src: string, generation: number): Promise<void> {
450
465
  this[PropertySymbol.complete] = false;
451
466
  this[PropertySymbol.naturalHeight] = 0;
452
467
  this[PropertySymbol.naturalWidth] = 0;
@@ -460,6 +475,10 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
460
475
  error = <Error>e;
461
476
  }
462
477
 
478
+ if (generation !== this.#loadGeneration) {
479
+ return;
480
+ }
481
+
463
482
  if (error || !response?.ok) {
464
483
  this[PropertySymbol.complete] = true;
465
484
  this.dispatchEvent(new this[PropertySymbol.window].Event('error'));
@@ -478,23 +497,34 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
478
497
  try {
479
498
  buffer = await response.buffer();
480
499
  } catch (e) {
500
+ if (generation !== this.#loadGeneration) {
501
+ return;
502
+ }
481
503
  this[PropertySymbol.complete] = true;
482
504
  this.dispatchEvent(new this[PropertySymbol.window].Event('error'));
483
505
  return;
484
506
  }
485
507
 
486
- this[PropertySymbol.complete] = true;
508
+ if (generation !== this.#loadGeneration) {
509
+ return;
510
+ }
511
+
487
512
  this[PropertySymbol.buffer] = buffer;
488
513
 
514
+ let dimensions = { width: 0, height: 0 };
489
515
  try {
490
- const dimensions = await ImageSize(buffer);
491
- this[PropertySymbol.naturalHeight] = dimensions.height;
492
- this[PropertySymbol.naturalWidth] = dimensions.width;
516
+ dimensions = await ImageSize(buffer);
493
517
  } catch (e) {
494
- this[PropertySymbol.naturalHeight] = 0;
495
- this[PropertySymbol.naturalWidth] = 0;
518
+ // Undecodable: no natural size.
496
519
  }
497
520
 
521
+ if (generation !== this.#loadGeneration) {
522
+ return;
523
+ }
524
+
525
+ this[PropertySymbol.naturalHeight] = dimensions.height;
526
+ this[PropertySymbol.naturalWidth] = dimensions.width;
527
+ this[PropertySymbol.complete] = true;
498
528
  this.dispatchEvent(new this[PropertySymbol.window].Event('load'));
499
529
  }
500
530
  }
@@ -12,10 +12,11 @@ import type { TGlobalMatchFunction } from './TGlobalMatchFunction.js';
12
12
  /**
13
13
  * Selector group RegExp.
14
14
  *
15
- * Group 1: Combinator (" ", ",", "+", ">", "̣~")
16
- * Group 2: Parentheses or brackets.
15
+ * Group 1: An escaped character (e.g. "\:", "\31 "), skipped.
16
+ * Group 2: Combinator (" ", ",", "+", ">", "̣~")
17
+ * Group 3: Parentheses, brackets or quotes.
17
18
  */
18
- const SELECTOR_GROUP_REGEXP = /(\s*[\s,+>~]\s*)|([\[\]\(\)"'])/g;
19
+ const SELECTOR_GROUP_REGEXP = /(\\[0-9a-fA-F]{1,6}\s?|\\[\s\S])|(\s*[\s,+>~]\s*)|([\[\]\(\)"'])/g;
19
20
 
20
21
  /**
21
22
  * Selector RegExp.
@@ -45,7 +46,7 @@ const SELECTOR_GROUP_REGEXP = /(\s*[\s,+>~]\s*)|([\[\]\(\)"'])/g;
45
46
  * Group 23: Pseudo element (e.g. "::after", "::-webkit-inner-spin-button").
46
47
  */
47
48
  const SELECTOR_REGEXP =
48
- /(\*)|([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;
49
+ /(\*)|((?:[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;
49
50
 
50
51
  /**
51
52
  * Selector pseudo RegExp.
@@ -55,11 +56,6 @@ const SELECTOR_REGEXP =
55
56
  */
56
57
  const SELECTOR_PSEUDO_REGEXP = /:([a-zA-Z-]+)|([()])/g;
57
58
 
58
- /**
59
- * Escaped Character RegExp.
60
- */
61
- const ESCAPED_CHARACTER_REGEXP = /\\/g;
62
-
63
59
  /**
64
60
  * CSS Escape RegExp.
65
61
  *
@@ -187,6 +183,11 @@ export default class SelectorParser {
187
183
 
188
184
  while ((match = regExp.exec(selector))) {
189
185
  if (match[1]) {
186
+ // An escaped character (CSS Syntax §4.3.7) is part of the selector it is in,
187
+ // never a combinator, bracket or quote.
188
+ continue;
189
+ }
190
+ if (match[2]) {
190
191
  // Matches combinator (" ", ",", "+", ">", "̣~")
191
192
  // We should ignore combinators that are inside parentheses, brackets or apostrophes
192
193
 
@@ -198,7 +199,7 @@ export default class SelectorParser {
198
199
  ) {
199
200
  const childSelector = selector.substring(lastIndex, match.index).trim();
200
201
 
201
- switch (match[1].trim()) {
202
+ switch (match[2].trim()) {
202
203
  case ',':
203
204
  selectorItem = this.getSelectorGroupItem(childSelector, combinator);
204
205
  if (!selectorItem) {
@@ -262,7 +263,7 @@ export default class SelectorParser {
262
263
  } else {
263
264
  // Matches parentheses or brackets.
264
265
 
265
- switch (match[2]) {
266
+ switch (match[3]) {
266
267
  case '(':
267
268
  if (depth.singleApostrophe === 0 && depth.doubleApostrophe === 0) {
268
269
  depth.parentheses++;
@@ -365,22 +366,22 @@ export default class SelectorParser {
365
366
  } else if (match[2]) {
366
367
  // Matches tag name, e.g. "div"
367
368
 
368
- selectorItem.tagName = match[2].toUpperCase();
369
+ selectorItem.tagName = SelectorParser.cssUnescape(match[2]).toUpperCase();
369
370
  } else if (match[3]) {
370
371
  // Matches ID, e.g. "#id"
371
372
 
372
- selectorItem.id = match[3].replace(ESCAPED_CHARACTER_REGEXP, '');
373
+ selectorItem.id = SelectorParser.cssUnescape(match[3]);
373
374
  } else if (match[5]) {
374
375
  // Matches class names, e.g. ".class1"
375
376
 
376
377
  selectorItem.classNames = selectorItem.classNames || [];
377
- selectorItem.classNames.push(match[5].replace(ESCAPED_CHARACTER_REGEXP, ''));
378
+ selectorItem.classNames.push(SelectorParser.cssUnescape(match[5]));
378
379
  } else if (match[7]) {
379
380
  // Matches attributes without value, e.g. [attr]
380
381
 
381
382
  selectorItem.attributes = selectorItem.attributes || [];
382
383
  selectorItem.attributes.push({
383
- name: match[7].replace(ESCAPED_CHARACTER_REGEXP, ''),
384
+ name: SelectorParser.cssUnescape(match[7]),
384
385
  operator: null,
385
386
  value: null,
386
387
  modifier: null,
@@ -393,7 +394,7 @@ export default class SelectorParser {
393
394
  const unescapedValue = SelectorParser.cssUnescape(value);
394
395
  selectorItem.attributes = selectorItem.attributes || [];
395
396
  selectorItem.attributes.push({
396
- name: match[9].replace(ESCAPED_CHARACTER_REGEXP, ''),
397
+ name: SelectorParser.cssUnescape(match[9]),
397
398
  operator: match[11] || null,
398
399
  value: unescapedValue,
399
400
  modifier: <'s'>match[15] || null,
@@ -409,7 +410,7 @@ export default class SelectorParser {
409
410
  const unescapedValue = SelectorParser.cssUnescape(match[19]);
410
411
  selectorItem.attributes = selectorItem.attributes || [];
411
412
  selectorItem.attributes.push({
412
- name: match[16].replace(ESCAPED_CHARACTER_REGEXP, ''),
413
+ name: SelectorParser.cssUnescape(match[16]),
413
414
  operator: match[18] || null,
414
415
  value: unescapedValue,
415
416
  modifier: null,
@@ -9568,7 +9568,7 @@ function buildOptimizer(constants, name) {
9568
9568
  ${globalObjectOptimizer}
9569
9569
  ${moduleLexicalOptimizer}
9570
9570
  return function() {
9571
- 'use strict';
9571
+ ${context.sloppyCode ? '' : "'use strict';"}
9572
9572
  return eval(arguments[0]);
9573
9573
  };
9574
9574
  }
@@ -9611,6 +9611,7 @@ const { Fail } = assert;
9611
9611
  moduleLexicals = {},
9612
9612
  globalTransforms = [],
9613
9613
  sloppyGlobalsMode = false,
9614
+ sloppyCode = false,
9614
9615
  }) => {
9615
9616
  const scopeTerminator = sloppyGlobalsMode
9616
9617
  ? createSloppyGlobalsScopeTerminator(globalObject)
@@ -9623,6 +9624,7 @@ const { Fail } = assert;
9623
9624
  moduleLexicals,
9624
9625
  globalObject,
9625
9626
  scopeTerminator,
9627
+ sloppyCode,
9626
9628
  });
9627
9629
 
9628
9630
  // Defer creating the actual evaluator to first use.
@@ -9715,6 +9717,14 @@ const nativeSuffix = ') { [native code] }';
9715
9717
  // use this top level state so that multiple calls to `tameFunctionToString` are
9716
9718
  // idempotent, rather than creating redundant indirections.
9717
9719
  let markVirtualizedNativeFunction;
9720
+ let sourceTextTransform;
9721
+ // Taken (and deleted) by the embedder, like the native-function marker.
9722
+ Object.defineProperty(globalThis, Symbol.for('ses.setFunctionSourceTextTransform'), {
9723
+ value: transform => {
9724
+ sourceTextTransform = transform;
9725
+ },
9726
+ configurable: true,
9727
+ });
9718
9728
 
9719
9729
  /**
9720
9730
  * Replace `Function.prototype.toString` with one that recognizes
@@ -9726,7 +9736,10 @@ let markVirtualizedNativeFunction;
9726
9736
 
9727
9737
  const tamingMethods = {
9728
9738
  toString() {
9729
- const str = functionToString(this);
9739
+ let str = functionToString(this);
9740
+ // The embedder may map the text of source it rewrote back to the
9741
+ // text the page wrote (see the registered symbol below).
9742
+ if (sourceTextTransform !== undefined) str = sourceTextTransform(str);
9730
9743
  if (
9731
9744
  stringEndsWith(str, nativeSuffix) ||
9732
9745
  !weaksetHas(virtualizedNativeFunctions, this)
@@ -9744,6 +9757,8 @@ let markVirtualizedNativeFunction;
9744
9757
  markVirtualizedNativeFunction = freeze(func =>
9745
9758
  weaksetAdd(virtualizedNativeFunctions, func),
9746
9759
  );
9760
+ // Function.prototype.toString is itself a built-in function.
9761
+ markVirtualizedNativeFunction(tamingMethods.toString);
9747
9762
  }
9748
9763
  return markVirtualizedNativeFunction;
9749
9764
  };$h͏_once.tameFunctionToString(tameFunctionToString);
@@ -12522,13 +12537,29 @@ const { quote: q } = assert;
12522
12537
 
12523
12538
 
12524
12539
 
12525
- const provideCompartmentEvaluator = (compartmentFields, options) => {
12526
- const { sloppyGlobalsMode = false, __moduleShimLexicals__ = undefined } =
12527
- options;
12540
+ // Evaluators for sloppyGlobalsMode / __sloppyCode__, one per compartment and
12541
+ // combination: building one scans the global object and compiles a new
12542
+ // evaluator function, which made every such evaluate() call cost hundreds of
12543
+ // microseconds.
12544
+ const sloppyEvaluators = new WeakMap();
12545
+
12546
+ const provideCompartmentEvaluator = (compartmentFields, options) => {
12547
+ const {
12548
+ sloppyGlobalsMode = false,
12549
+ __moduleShimLexicals__ = undefined,
12550
+ __sloppyCode__ = false,
12551
+ } = options;
12528
12552
 
12529
12553
  let safeEvaluate;
12530
12554
 
12531
- if (__moduleShimLexicals__ === undefined && !sloppyGlobalsMode) {
12555
+ const cacheKey = __moduleShimLexicals__ === undefined
12556
+ ? `${Boolean(sloppyGlobalsMode)}:${Boolean(__sloppyCode__)}` : undefined;
12557
+ let cached = sloppyEvaluators.get(compartmentFields);
12558
+ if (cacheKey !== undefined && cached && cached.has(cacheKey)) {
12559
+ return { safeEvaluate: cached.get(cacheKey) };
12560
+ }
12561
+
12562
+ if (__moduleShimLexicals__ === undefined && !sloppyGlobalsMode && !__sloppyCode__) {
12532
12563
  ({ safeEvaluate } = compartmentFields);
12533
12564
  } else {
12534
12565
  // The scope proxy or global lexicals are different from the
@@ -12560,7 +12591,15 @@ const { quote: q } = assert;
12560
12591
  moduleLexicals,
12561
12592
  globalTransforms,
12562
12593
  sloppyGlobalsMode,
12594
+ sloppyCode: __sloppyCode__,
12563
12595
  }));
12596
+ if (cacheKey !== undefined) {
12597
+ if (!cached) {
12598
+ cached = new Map();
12599
+ sloppyEvaluators.set(compartmentFields, cached);
12600
+ }
12601
+ cached.set(cacheKey, safeEvaluate);
12602
+ }
12564
12603
  }
12565
12604
 
12566
12605
  return { safeEvaluate };
@@ -15138,6 +15177,13 @@ globalThis.repairIntrinsics = options => {
15138
15177
 
15139
15178
  const markVirtualizedNativeFunction = tameFunctionToString();
15140
15179
 
15180
+ // The embedder takes this once (and deletes it) to present its own
15181
+ // host-implemented Web APIs as built-in functions, as browsers do.
15182
+ Object.defineProperty(globalThis, Symbol.for('ses.markVirtualizedNativeFunction'), {
15183
+ value: markVirtualizedNativeFunction,
15184
+ configurable: true,
15185
+ });
15186
+
15141
15187
  const muteReporter = chooseReporter('none');
15142
15188
 
15143
15189
  // @ts-ignore Compartment is definitely on globalThis.