buninu-browser 0.7.0 → 0.9.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.
Files changed (142) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +90 -1
  3. package/buninu-browser.js +1 -1
  4. package/frontends/wayland/README.md +35 -2
  5. package/frontends/wayland/client.js +8 -6
  6. package/frontends/wayland/clipboard.js +1 -1
  7. package/frontends/wayland/index.js +1 -1
  8. package/frontends/wayland/input.js +1 -1
  9. package/frontends/wayland/resource-io.js +6 -0
  10. package/frontends/wayland/transport-posix.js +120 -0
  11. package/frontends/wayland/transport-win32.js +9 -0
  12. package/frontends/wayland/transport.js +3 -120
  13. package/frontends/wayland/win32-resources.js +80 -0
  14. package/frontends/wayland/win32-wire.js +62 -0
  15. package/lib/audio/biquad.js +25 -0
  16. package/lib/audio/piano.js +6 -3
  17. package/lib/audio/playback-buffer.js +5 -5
  18. package/lib/audio/pulse-client.js +24 -5
  19. package/lib/audio/web-audio.js +570 -0
  20. package/lib/browser-dialog.js +126 -0
  21. package/lib/cdp-server.js +43 -11
  22. package/lib/file-picker.js +48 -0
  23. package/lib/headless-shell.js +34 -12
  24. package/lib/layout/text-layout.js +112 -19
  25. package/lib/media/mp4-frame-times.js +93 -0
  26. package/lib/paint/direct-canvas.ts +39 -0
  27. package/lib/paint/display-list.js +37 -12
  28. package/lib/paint/raster-readback.js +16 -7
  29. package/lib/renderer/animation-frames.js +10 -5
  30. package/lib/renderer/bindings.js +69 -16
  31. package/lib/renderer/canvas-2d.js +265 -15
  32. package/lib/renderer/canvas-round-rect.js +36 -0
  33. package/lib/renderer/css-animations.js +173 -0
  34. package/lib/renderer/dialog-client.js +30 -0
  35. package/lib/renderer/dialog-worker.js +35 -0
  36. package/lib/renderer/eval-environment.js +28 -0
  37. package/lib/renderer/file-input.js +49 -0
  38. package/lib/renderer/host.js +33 -4
  39. package/lib/renderer/html-drag.js +77 -0
  40. package/lib/renderer/image-loading.js +45 -2
  41. package/lib/renderer/main.js +4 -0
  42. package/lib/renderer/module-transform.js +5 -3
  43. package/lib/renderer/page-builtins.js +59 -0
  44. package/lib/renderer/page-realm.js +177 -40
  45. package/lib/renderer/page-renderer.js +397 -49
  46. package/lib/renderer/piano-input.js +3 -1
  47. package/lib/renderer/raster-surface.js +18 -0
  48. package/lib/renderer/script-rewrite.js +165 -9
  49. package/lib/renderer/video-presentation.js +5 -4
  50. package/lib/renderer/webgl/clip.js +24 -0
  51. package/lib/renderer/webgl/context.js +158 -0
  52. package/lib/renderer/webgl/cpu-fragment.js +211 -0
  53. package/lib/renderer/webgl/draw-cache.js +22 -0
  54. package/lib/renderer/webgl/pipeline.js +70 -0
  55. package/lib/renderer/webgl/shaders.js +58 -0
  56. package/lib/renderer/webgl/textures.js +43 -0
  57. package/lib/renderer/window-named-properties.js +46 -0
  58. package/lib/screenshot-cli.js +4 -7
  59. package/lib/style/animation-values.js +29 -0
  60. package/lib/style/computed-style.js +109 -13
  61. package/lib/style/transform.js +44 -0
  62. package/lib/window-size.js +10 -0
  63. package/package.json +1 -1
  64. package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
  65. package/patches/happy-dom/README.md +16 -0
  66. package/test/animation-frames.test.js +15 -0
  67. package/test/canvas-2d.test.js +252 -4
  68. package/test/canvas-frame-pacing.test.js +7 -0
  69. package/test/cdp-viewers.test.js +36 -0
  70. package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
  71. package/test/classic-games-patches/README.md +156 -0
  72. package/test/classic-games-scenarios.js +133 -0
  73. package/test/clipboard.test.js +16 -0
  74. package/test/computed-style.test.js +18 -0
  75. package/test/css-animation-lifecycle.test.js +28 -0
  76. package/test/css-animations.test.js +83 -0
  77. package/test/direct-canvas.test.js +39 -0
  78. package/test/dynamic-inline-handlers.test.js +25 -0
  79. package/test/eval-declarations.test.js +22 -0
  80. package/test/file-picker.test.js +34 -0
  81. package/test/fixtures/canvas-damage.js +23 -0
  82. package/test/fixtures/canvas-frame-pacing.js +35 -0
  83. package/test/fixtures/direct-canvas.js +96 -0
  84. package/test/fixtures/javascript-dialogs.html +8 -0
  85. package/test/fixtures/media-audio-lifetime.js +65 -0
  86. package/test/fixtures/renderer-memory.js +7 -5
  87. package/test/fixtures/video-black-replay.js +8 -6
  88. package/test/fixtures/video-slow-end.js +39 -0
  89. package/test/fixtures/web-audio-renderer.js +45 -0
  90. package/test/flex.test.js +26 -0
  91. package/test/grid.test.js +13 -0
  92. package/test/headless-shell.test.js +9 -0
  93. package/test/headless-window-size.test.js +28 -0
  94. package/test/html-rcdata.test.js +35 -0
  95. package/test/image-decode.test.js +18 -0
  96. package/test/javascript-dialog.test.js +83 -0
  97. package/test/manual-breakout-paddle.js +42 -0
  98. package/test/manual-breakout-static.js +35 -0
  99. package/test/manual-classic-games.js +41 -0
  100. package/test/manual-clumsy-bird-three-pipes.js +81 -0
  101. package/test/manual-clumsy-bird.js +52 -0
  102. package/test/manual-hextris.js +30 -0
  103. package/test/manual-numbered-music.js +24 -0
  104. package/test/manual-t-rex-runner.js +49 -0
  105. package/test/manual-threejs-scene.js +64 -0
  106. package/test/manual-webgl-benchmark.js +27 -0
  107. package/test/manual-webgl-cpu.html +26 -0
  108. package/test/media-audio-lifetime.test.js +8 -0
  109. package/test/mp4-frame-times.test.js +33 -0
  110. package/test/page-scripts.test.js +96 -1
  111. package/test/piano-input.test.js +15 -2
  112. package/test/piano.test.js +11 -2
  113. package/test/playback-buffer.test.js +11 -0
  114. package/test/pointer-drag.test.js +33 -0
  115. package/test/positioning.test.js +82 -0
  116. package/test/pulse-client.test.js +51 -2
  117. package/test/raster-readback.test.js +14 -0
  118. package/test/script-rewrite.test.js +61 -3
  119. package/test/video-slow-end.test.js +6 -0
  120. package/test/viewport-keyboard-overflow.test.js +25 -0
  121. package/test/wayland-win32-resources.test.js +47 -0
  122. package/test/wayland-win32-wire.test.js +59 -0
  123. package/test/web-audio-renderer.test.js +8 -0
  124. package/test/web-audio.test.js +264 -0
  125. package/test/webgl-clip.test.js +9 -0
  126. package/test/webgl-cpu-fragment.test.js +43 -0
  127. package/test/window-named-properties.test.js +41 -0
  128. package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
  129. package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
  130. package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
  131. package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
  132. package/vendor/land-renderer/LICENSE +21 -0
  133. package/vendor/land-renderer/README.md +14 -0
  134. package/vendor/land-renderer/direct-compositor.ts +443 -0
  135. package/vendor/land-renderer/frame.js +88 -0
  136. package/vendor/land-renderer/png-fast.js +85 -0
  137. package/vendor/land-renderer/raw-frames.js +66 -0
  138. package/vendor/land-renderer/render-types.ts +7 -0
  139. package/vendor/land-renderer/renderer.ts +134 -0
  140. package/vendor/land-renderer/snapshot.js +37 -0
  141. package/vendor/land-renderer/viewport.js +18 -0
  142. package/vendor/ses/dist/ses.mjs +1 -1
@@ -1,3 +1,4 @@
1
+ import {animationProperty, animationLists, animationShorthand} from './animation-values.js';
1
2
  /**
2
3
  * Initial style model informed by TermDOM's UA/cascade tests and UA element
3
4
  * categories (bikeshaving/termdom, MIT, pinned in Architecture.md). The data
@@ -162,6 +163,7 @@ const INITIAL = Object.freeze({
162
163
  minHeight: "auto",
163
164
  maxHeight: "none",
164
165
  boxSizing: "content-box",
166
+ aspectRatio: null,
165
167
  // Used border widths (0 where border-style is none/hidden), top/right/bottom/left.
166
168
  borderWidths: Object.freeze([0, 0, 0, 0]),
167
169
  borderColors: Object.freeze([null, null, null, null]),
@@ -225,6 +227,8 @@ const INITIAL = Object.freeze({
225
227
  overflowWrap: "normal",
226
228
  wordBreak: "normal",
227
229
  opacity: 1,
230
+ backfaceVisibility: "visible",
231
+ transformStyle: "flat",
228
232
  overflowX: "visible",
229
233
  overflowY: "visible",
230
234
  // Scroll containers (CSS Overflow 3 §3): "auto" for auto/scroll/overlay,
@@ -332,6 +336,7 @@ let environment = DEFAULT_ENVIRONMENT;
332
336
 
333
337
  export class StyleEngine {
334
338
  #measureText;
339
+ #sampleStyle;
335
340
  #deferHidden;
336
341
  #hiddenRoots = new WeakSet();
337
342
  #hiddenAncestors = new WeakMap();
@@ -355,7 +360,8 @@ export class StyleEngine {
355
360
  #parsedSheetsKey = "";
356
361
 
357
362
  // Renderers may defer invisible descendants; standalone callers remain eager.
358
- constructor({ measureText = null, deferHidden = false } = {}) {
363
+ constructor({ measureText = null, deferHidden = false, sampleStyle = null } = {}) {
364
+ this.#sampleStyle = sampleStyle;
359
365
  this.#deferHidden = deferHidden;
360
366
  this.#measureText = measureText && ((text, style) => {
361
367
  this.#usesFontMetrics = true;
@@ -663,6 +669,7 @@ export class StyleEngine {
663
669
  let customProperties = withCustomProperties(parentCustomProperties ?? EMPTY_MAP, declarations);
664
670
  const dependencies = { inheritsParent: false };
665
671
  let style = computeElementStyle(element, parentStyle, declarations, customProperties, dependencies, true);
672
+ if (this.#sampleStyle && !target) style = this.#sampleStyle(element, style);
666
673
  // A descendant may be visited because an ancestor's selector state
667
674
  // changed even though its final computed values did not. Preserve the
668
675
  // immutable identities so retained render and layout subtrees survive.
@@ -1172,6 +1179,45 @@ export function parseStyleSheet(source, viewport = environment, counter = { orde
1172
1179
  return rules;
1173
1180
  }
1174
1181
 
1182
+ /** Named keyframes share the stylesheet parser's balanced blocks and media filtering. */
1183
+ export function extractAnimationKeyframes(source, viewport = environment) {
1184
+ const result = new Map();
1185
+ const css = String(source).replace(/\/\*[\s\S]*?\*\//g, "");
1186
+ function visit(start, end) {
1187
+ let index = start;
1188
+ while (index < end) {
1189
+ const open = findTopLevel(css, "{", index, end);
1190
+ const semicolon = findTopLevel(css, ";", index, end);
1191
+ if (semicolon >= 0 && (open < 0 || semicolon < open)) { index=semicolon+1;continue; }
1192
+ if (open < 0) break;
1193
+ const close = matchingBrace(css, open, end);
1194
+ const prelude = css.slice(index, open).trim();
1195
+ const match = /^@(?:-webkit-)?keyframes\s+([\w-]+|"[^"]+"|'[^']+')$/i.exec(prelude);
1196
+ if (match) {
1197
+ const frames = [];
1198
+ let offset = open + 1;
1199
+ while (offset < close) {
1200
+ const from = findTopLevel(css, "{", offset, close);
1201
+ if (from < 0) break;
1202
+ const to = matchingBrace(css, from, close);
1203
+ const declarations = parseDeclarations(css.slice(from + 1, to));
1204
+ for (const selector of css.slice(offset, from).split(",")) {
1205
+ const text = selector.trim();
1206
+ const at = text === "from" ? 0 : text === "to" ? 1 : /^(?:\d*\.)?\d+%$/.test(text) ? parseFloat(text)/100 : NaN;
1207
+ if (at >= 0 && at <= 1) frames.push({offset:at, declarations});
1208
+ }
1209
+ offset = to + 1;
1210
+ }
1211
+ result.set(match[1].replace(/^['"]|['"]$/g, ""), frames.sort((a,b)=>a.offset-b.offset));
1212
+ } else if (/^@supports\b/i.test(prelude) || /^@layer(?:\s|$)/i.test(prelude)
1213
+ || (/^@media\b/i.test(prelude) && mediaQueryMatches(prelude.slice(6).trim(), viewport))) visit(open+1, close);
1214
+ index = close + 1;
1215
+ }
1216
+ }
1217
+ visit(0,css.length);
1218
+ return result;
1219
+ }
1220
+
1175
1221
  /** Font resource descriptors, separate from selector rules (CSS Fonts §4). */
1176
1222
  export function extractFontFaces(source, baseURL, viewport = environment) {
1177
1223
  const css = String(source).replace(/\/\*[\s\S]*?\*\//g, "");
@@ -1460,6 +1506,15 @@ function mediaLength(value) {
1460
1506
  return Number(match[1]) * (match[2] && match[2] !== "px" ? DEFAULT_ENVIRONMENT.rootFontSize : 1);
1461
1507
  }
1462
1508
 
1509
+ function parseAspectRatio(value) {
1510
+ if (value.toLowerCase() === "auto") return null;
1511
+ const match = /^(?:(auto)\s+)?([+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)(?:\s*\/\s*([+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?))?(?:\s+(auto))?$/i.exec(value);
1512
+ if (!match || match[1] && match[4]) return undefined;
1513
+ const numerator = Number(match[2]), denominator = Number(match[3] ?? 1);
1514
+ return Number.isFinite(numerator) && Number.isFinite(denominator)
1515
+ ? Object.freeze({ numerator, denominator, auto: Boolean(match[1] || match[4]) }) : undefined;
1516
+ }
1517
+
1463
1518
  export function parseDeclarations(source) {
1464
1519
  const declarations = new Map();
1465
1520
  let declarationIndex = 0;
@@ -1468,11 +1523,13 @@ export function parseDeclarations(source) {
1468
1523
  if (colon < 1) continue;
1469
1524
  // Custom property names are case-sensitive (CSS Variables 1 §2).
1470
1525
  const rawName = part.slice(0, colon).trim();
1471
- const name = rawName.startsWith("--") ? rawName : rawName.toLowerCase();
1526
+ const name = rawName.startsWith("--") ? rawName : animationProperty(rawName.toLowerCase());
1472
1527
  let value = part.slice(colon + 1).trim();
1473
1528
  const important = /\s*!important\s*$/i.test(value);
1474
1529
  value = value.replace(/\s*!important\s*$/i, "").trim();
1475
1530
  if (!name || !value || (declarations.get(name)?.important && !important)) continue;
1531
+ if (name === "aspect-ratio" && !CSS_WIDE_KEYWORDS.has(value) && !/var\(/i.test(value)
1532
+ && parseAspectRatio(value) === undefined) continue;
1476
1533
  declarations.set(name, { name, value, important, declarationIndex: declarationIndex++ });
1477
1534
  }
1478
1535
  return declarations;
@@ -1545,6 +1602,17 @@ function setProperty(style, name, value, parentStyle = null) {
1545
1602
  }
1546
1603
  if (typeof value !== "string") return;
1547
1604
  switch (name) {
1605
+ case "animation":
1606
+ style.cssAnimations = animationShorthand(value);
1607
+ style.cssAnimationLonghands = {};
1608
+ break;
1609
+ case "animation-name": case "animation-duration": case "animation-delay":
1610
+ case "animation-iteration-count": case "animation-direction": case "animation-fill-mode":
1611
+ case "animation-play-state": case "animation-timing-function": {
1612
+ const key = name.slice(10).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
1613
+ style.cssAnimationLonghands = {...style.cssAnimationLonghands, [key]: animationLists(value)};
1614
+ break;
1615
+ }
1548
1616
  case "direction":
1549
1617
  if (value === "ltr" || value === "rtl") style.direction = value;
1550
1618
  break;
@@ -1795,6 +1863,12 @@ function setProperty(style, name, value, parentStyle = null) {
1795
1863
  style.translate = Object.freeze({x:lengths[0],y:lengths[1] ?? 0});
1796
1864
  break;
1797
1865
  }
1866
+ case "backface-visibility":
1867
+ if(["visible","hidden"].includes(value))style.backfaceVisibility=value;
1868
+ break;
1869
+ case "transform-style":
1870
+ if(["flat","preserve-3d"].includes(value))style.transformStyle=value;
1871
+ break;
1798
1872
  case "transform-origin":
1799
1873
  style.transformOrigin = value.toLowerCase().replace(/\s+/g, " ");
1800
1874
  break;
@@ -1992,13 +2066,18 @@ function setProperty(style, name, value, parentStyle = null) {
1992
2066
  = Object.freeze([lengths[0], lengths[1] ?? lengths[0]]);
1993
2067
  break;
1994
2068
  }
2069
+ case "aspect-ratio": {
2070
+ const ratio = parseAspectRatio(value);
2071
+ if (ratio !== undefined) style.aspectRatio = ratio;
2072
+ break;
2073
+ }
1995
2074
  case "box-sizing": if (["content-box", "border-box"].includes(value)) style.boxSizing = value; break;
1996
2075
  case "flex-flow": {
1997
2076
  const parts = value.split(/\s+/);
1998
2077
  let direction = null;
1999
2078
  let wrap = null;
2000
2079
  for (const part of parts) {
2001
- if (["row", "column"].includes(part) && direction == null) direction = part;
2080
+ if (["row", "column", "row-reverse", "column-reverse"].includes(part) && direction == null) direction = part;
2002
2081
  else if (["nowrap", "wrap"].includes(part) && wrap == null) wrap = part;
2003
2082
  else return;
2004
2083
  }
@@ -2008,7 +2087,7 @@ function setProperty(style, name, value, parentStyle = null) {
2008
2087
  }
2009
2088
  break;
2010
2089
  }
2011
- case "flex-direction": if (["row", "column"].includes(value)) style.flexDirection = value; break;
2090
+ case "flex-direction": if (["row", "column", "row-reverse", "column-reverse"].includes(value)) style.flexDirection = value; break;
2012
2091
  case "flex-wrap": if (["nowrap", "wrap"].includes(value)) style.flexWrap = value; break;
2013
2092
  case "justify-content": {
2014
2093
  const justify = FLEX_ALIGNMENT[value] ?? value;
@@ -2536,11 +2615,13 @@ function pseudoElement(pseudo) {
2536
2615
  * after a scale uses that scaled coordinate system. Percentages remain lengths
2537
2616
  * until layout knows the element's own border box. Other functions are ignored.
2538
2617
  */
2539
- function parseTransform(value, fontSize) {
2618
+ export function parseTransform(value, fontSize) {
2540
2619
  let x = 0;
2541
2620
  let y = 0;
2542
2621
  let scaleX = 1;
2543
2622
  let scaleY = 1;
2623
+ let b = 0, c = 0, affine = false;
2624
+ const operations=[];
2544
2625
  let found = false;
2545
2626
  const functions = /\b([a-z][a-z0-9]*)\(/gi;
2546
2627
  let match;
@@ -2560,24 +2641,39 @@ function parseTransform(value, fontSize) {
2560
2641
  const args = parts.map(part => computedLength(part, fontSize, true));
2561
2642
  const max = kind === "translate3d" ? 3 : kind === "translate" ? 2 : 1;
2562
2643
  if (!args.length || args.length > max || args.some(part => !Number.isFinite(part) && !part?.unit)) return undefined;
2563
- if (kind === "translatey") y = addTransformLength(y, scaleTransformLength(args[0], scaleY));
2564
- else {
2565
- x = addTransformLength(x, scaleTransformLength(args[0], scaleX));
2566
- if (kind !== "translatex") y = addTransformLength(y, scaleTransformLength(args[1] ?? 0, scaleY));
2567
- }
2644
+ const tx = kind === "translatey" ? 0 : args[0];
2645
+ const ty = kind === "translatey" ? args[0] : kind === "translatex" ? 0 : args[1] ?? 0;
2646
+ operations.push(Object.freeze({kind:"translate",x:tx,y:ty}));
2647
+ x = addTransformLength(x, addTransformLength(scaleTransformLength(tx, scaleX), scaleTransformLength(ty, c)));
2648
+ y = addTransformLength(y, addTransformLength(scaleTransformLength(tx, b), scaleTransformLength(ty, scaleY)));
2568
2649
  } else if (["scale", "scalex", "scaley", "scale3d"].includes(kind)) {
2569
2650
  const args = parts.map(part => part.endsWith("%") ? Number(part.slice(0, -1)) / 100 : Number(part));
2570
2651
  const max = kind === "scale3d" ? 3 : kind === "scale" ? 2 : 1;
2571
2652
  if (!args.length || args.length > max || args.some(part => !Number.isFinite(part))) return undefined;
2572
- if (kind === "scalex") scaleX *= args[0];
2573
- else if (kind === "scaley") scaleY *= args[0];
2574
- else { scaleX *= args[0]; scaleY *= args[1] ?? args[0]; }
2653
+ const sx = kind === "scaley" ? 1 : args[0], sy = kind === "scalex" ? 1 : args[1] ?? args[0];
2654
+ operations.push(Object.freeze({kind:"scale",x:sx,y:sy}));
2655
+ scaleX *= sx; b *= sx; c *= sy; scaleY *= sy;
2656
+ } else if (["rotate","rotatez","rotatex","rotatey"].includes(kind)) {
2657
+ const angle = /^([+-]?(?:\d*\.)?\d+)(deg|rad|grad|turn)?$/i.exec(parts[0]?.trim());
2658
+ if (parts.length !== 1 || !angle || (!angle[2] && Number(angle[1]) !== 0)) return undefined;
2659
+ const radians = Number(angle[1]) * ({deg:Math.PI/180,rad:1,grad:Math.PI/200,turn:2*Math.PI}[angle[2]?.toLowerCase()] ?? 1);
2660
+ if(kind==="rotatex"||kind==="rotatey"){
2661
+ operations.push(Object.freeze({kind,angle:radians}));affine=true;
2662
+ const factor=Math.abs(Math.cos(radians))<1e-14?0:Math.cos(radians);
2663
+ if(kind==="rotatey"){scaleX*=factor;b*=factor;}else{c*=factor;scaleY*=factor;}
2664
+ found=true;continue;
2665
+ }
2666
+ operations.push(Object.freeze({kind:"rotate",angle:radians}));
2667
+ const cs = Math.cos(radians), sn = Math.sin(radians), a = scaleX, d = scaleY, oldB = b, oldC = c;
2668
+ scaleX = a*cs+oldC*sn; c = -a*sn+oldC*cs;
2669
+ b = oldB*cs+d*sn; scaleY = -oldB*sn+d*cs; affine = true;
2575
2670
  } else {
2576
2671
  continue;
2577
2672
  }
2578
2673
  found = true;
2579
2674
  }
2580
2675
  if (!found) return null;
2676
+ if (affine) return Object.freeze({x:0,y:0,affine:Object.freeze({operations:Object.freeze(operations)})});
2581
2677
  return Object.freeze(scaleX === 1 && scaleY === 1 ? { x, y } : { x, y, scaleX, scaleY });
2582
2678
  }
2583
2679
 
@@ -0,0 +1,44 @@
1
+ /** Resolve CSS affine transforms after the element's reference box is known. */
2
+ export function transformLength(value,reference){
3
+ if(typeof value==='number')return value;
4
+ if(value?.unit==='%')return reference*value.value/100;
5
+ if(value?.kind==='calc')return value.px+reference*value.percent/100;
6
+ if(value?.kind==='scale')return value.factor*transformLength(value.value,reference);
7
+ const v=value?.values?.map(x=>transformLength(x,reference))??[];
8
+ if(value?.kind==='sum')return v.reduce((a,b)=>a+b,0);
9
+ if(value?.kind==='min')return Math.min(...v);
10
+ if(value?.kind==='max')return Math.max(...v);
11
+ return v.length?Math.max(v[0],Math.min(v[1],v[2])):0;
12
+ }
13
+ export function multiplyTransform(outer,inner){
14
+ const {a,b=0,c=0,d,e,f}=outer,{a:A,b:B=0,c:C=0,d:D,e:E,f:F}=inner;
15
+ return {a:a*A+c*B,b:b*A+d*B,c:a*C+c*D,d:b*C+d*D,e:a*E+c*F+e,f:b*E+d*F+f};
16
+ }
17
+ export function resolveAffine(transform,width,height){
18
+ let matrix={a:1,b:0,c:0,d:1,e:0,f:0};
19
+ for(const op of transform.operations){
20
+ let next;
21
+ if(op.kind==='translate')next={a:1,d:1,e:transformLength(op.x,width),f:transformLength(op.y,height)};
22
+ else if(op.kind==='scale')next={a:op.x,d:op.y,e:0,f:0};
23
+ else if(op.kind==='matrix')next=op.matrix;
24
+ else if(op.kind==='rotatex'||op.kind==='rotatey'){const value=Math.cos(op.angle),cs=Math.abs(value)<1e-14?0:value;next={a:op.kind==='rotatey'?cs:1,d:op.kind==='rotatex'?cs:1,e:0,f:0};}
25
+ else {const clean=n=>Math.abs(n)<1e-14?0:n,cs=clean(Math.cos(op.angle)),sn=clean(Math.sin(op.angle));next={a:cs,b:sn,c:-sn,d:cs,e:0,f:0};}
26
+ matrix=multiplyTransform(matrix,next);
27
+ }
28
+ return matrix;
29
+ }
30
+
31
+ /** Rotational part for backface tests in a preserve-3d rendering context. */
32
+ export function rotationMatrix3D(transform){
33
+ let matrix=[1,0,0,0,1,0,0,0,1];
34
+ for(const op of transform?.affine?.operations??[]){
35
+ if(!['rotate','rotatex','rotatey'].includes(op.kind))continue;
36
+ const c=Math.cos(op.angle),s=Math.sin(op.angle);
37
+ const next=op.kind==='rotatex'?[1,0,0,0,c,-s,0,s,c]:op.kind==='rotatey'?[c,0,s,0,1,0,-s,0,c]:[c,-s,0,s,c,0,0,0,1];
38
+ matrix=multiplyRotation3D(matrix,next);
39
+ }
40
+ return matrix;
41
+ }
42
+ export function multiplyRotation3D(a,b){
43
+ return a.map((_,index)=>{const r=Math.floor(index/3),c=index%3;return a[r*3]*b[c]+a[r*3+1]*b[c+3]+a[r*3+2]*b[c+6];});
44
+ }
@@ -0,0 +1,10 @@
1
+ /** Shared CLI viewport parsing for persistent headless and screenshot modes. */
2
+ export function parseWindowSize(args) {
3
+ const value = args.find(arg => arg.startsWith('--window-size='))?.slice('--window-size='.length);
4
+ if (value === undefined) return {width: 800, height: 600};
5
+ const match = value.match(/^(\d+)[,x](\d+)$/i);
6
+ const width = Number(match?.[1]), height = Number(match?.[2]);
7
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 1 || height < 1)
8
+ throw new Error('--window-size must be WIDTH,HEIGHT with positive integers');
9
+ return {width, height};
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-browser",
3
- "version": "0.7.0",
3
+ "version": "0.9.1",
4
4
  "description": "Buninu Browser is a browser engine written in JavaScript and WebAssembly for Bun.",
5
5
  "keywords": [
6
6
  "buninu",
@@ -0,0 +1,29 @@
1
+ diff --git a/packages/happy-dom/src/html-parser/HTMLParser.ts b/packages/happy-dom/src/html-parser/HTMLParser.ts
2
+ index 8d56e4f..44b4efc 100755
3
+ --- a/packages/happy-dom/src/html-parser/HTMLParser.ts
4
+ +++ b/packages/happy-dom/src/html-parser/HTMLParser.ts
5
+ @@ -327,6 +327,7 @@ export default class HTMLParser {
6
+ }
7
+
8
+ if (this.currentNode && this.readState === MarkupReadStateEnum.any &&
9
+ + (<Element>this.currentNode)[PropertySymbol.namespaceURI] === NamespaceURI.html &&
10
+ HTMLElementConfig[(<Element>this.currentNode)[PropertySymbol.localName]]?.contentModel === HTMLElementConfigContentModelEnum.rcdata) {
11
+ const tag = (<Element>this.currentNode)[PropertySymbol.localName];
12
+ const end = new RegExp('</' + tag + '(?=[\\t\\n\\f\\r />])[^>]*>', 'gi');
13
+ @@ -478,12 +479,14 @@ export default class HTMLParser {
14
+
15
+ const tagName = this.nextElement![PropertySymbol.tagName]!;
16
+ const lowerTagName = tagName.toLowerCase();
17
+ - const config = HTMLElementConfig[lowerTagName];
18
+ + const config = this.nextElement![PropertySymbol.namespaceURI] === NamespaceURI.html
19
+ + ? HTMLElementConfig[lowerTagName] : undefined;
20
+ let previousCurrentNode: Node | null = null;
21
+
22
+ while (previousCurrentNode !== this.rootNode) {
23
+ const parentLowerTagName = (<Element>this.currentNode)[PropertySymbol.tagName]?.toLowerCase();
24
+ - const parentConfig = parentLowerTagName ? HTMLElementConfig[parentLowerTagName] : null;
25
+ + const parentConfig = parentLowerTagName && (<Element>this.currentNode)[PropertySymbol.namespaceURI] === NamespaceURI.html
26
+ + ? HTMLElementConfig[parentLowerTagName] : null;
27
+
28
+ if (previousCurrentNode === this.currentNode) {
29
+ throw new Error(
@@ -37,6 +37,13 @@ git -C vendor/happy-dom apply --reverse --check ../../patches/happy-dom/0005-inv
37
37
  git -C vendor/happy-dom apply --reverse --check ../../patches/happy-dom/0006-parse-css-escapes-in-selectors.patch
38
38
  ```
39
39
 
40
+ The incremental `0007-scope-content-models-to-html-namespace.patch` applies to
41
+ our current vendored parser (including its RCDATA compatibility fixes):
42
+
43
+ ```sh
44
+ git -C vendor/happy-dom apply ../../patches/happy-dom/0007-scope-content-models-to-html-namespace.patch
45
+ ```
46
+
40
47
  ## Patch manifest
41
48
 
42
49
  | Patch | Modified upstream file | Reason | Verification |
@@ -74,3 +81,12 @@ Keep each local Happy DOM change in a separate numbered patch and add it to
74
81
  this table with its reason and focused test. When updating Happy DOM, regenerate
75
82
  and re-check the queue against the new pinned commit before changing the version
76
83
  record in `THIRD_PARTY_NOTICES.md`.
84
+
85
+ ### `0007`: HTML content models do not apply to SVG
86
+
87
+ `HTMLParser.ts` must use HTML content-model rules and the RCDATA fast path
88
+ only for nodes in the HTML namespace. Treating an SVG `<title>` as HTML
89
+ RCDATA repeatedly consumed later markup without closing the lowercase SVG
90
+ node, deleting following controls and preventing Hextris initialization.
91
+ Validation: `bun test test/html-rcdata.test.js`; live game
92
+ <https://hextris.io/>. The same rule preserves SVG title text and following SVG/HTML siblings.
@@ -65,3 +65,18 @@ test("actual SES frame callbacks match the browser callback order and lifecycle"
65
65
  expect({ code, error }).toEqual({ code: 0, error: "" });
66
66
  expect(out).toContain("animation frame ordering and lifecycle passed");
67
67
  }, 15000);
68
+
69
+ frameTest('presentation follows all frame microtasks and precedes scheduling the next frame',()=>{
70
+ let now=0,wake=null,value='initial';const paints=[],delays=[];
71
+ const timers={schedule(callback,delay){wake=callback;delays.push(delay);return 1},cancel(){wake=null},dispose(){wake=null}};
72
+ const frames=createAnimationFrames({now:()=>now,frameTimers:timers,invoke:callback=>callback(),afterFrame(){
73
+ paints.push(value);now+=25;
74
+ if(paints.length===1)frames.request(()=>{value='next'});
75
+ }});
76
+ frames.request(()=>{value='callback';Promise.resolve().then(()=>{value='microtask'})});
77
+ let callback=wake;wake=null;now=16;callback();
78
+ expect(paints).toEqual(['microtask']);expect(delays).toEqual([16,0]);
79
+ callback=wake;wake=null;callback();expect(paints).toEqual(['microtask','next']);
80
+ frames.request(()=>frames.dispose());callback=wake;wake=null;callback();
81
+ expect(paints).toEqual(['microtask','next']);expect(wake).toBeNull();
82
+ });