buninu-browser 0.6.0 → 0.9.0
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 +5 -0
- package/README.md +94 -4
- package/buninu-browser.js +1 -1
- package/frontends/wayland/README.md +35 -2
- package/frontends/wayland/client.js +8 -6
- package/frontends/wayland/clipboard.js +1 -1
- package/frontends/wayland/index.js +1 -1
- package/frontends/wayland/input.js +1 -1
- package/frontends/wayland/resource-io.js +6 -0
- package/frontends/wayland/transport-posix.js +120 -0
- package/frontends/wayland/transport-win32.js +9 -0
- package/frontends/wayland/transport.js +3 -120
- package/frontends/wayland/win32-resources.js +80 -0
- package/frontends/wayland/win32-wire.js +62 -0
- package/index.js +10 -6
- package/lib/audio/biquad.js +25 -0
- package/lib/audio/piano.js +6 -3
- package/lib/audio/playback-buffer.js +5 -5
- package/lib/audio/pulse-client.js +24 -5
- package/lib/audio/web-audio.js +570 -0
- package/lib/browser-dialog.js +116 -0
- package/lib/cdp-server.js +128 -72
- package/lib/file-picker.js +48 -0
- package/lib/headless-shell.js +34 -12
- package/lib/layout/text-layout.js +112 -19
- package/lib/media/mp4-frame-times.js +93 -0
- package/lib/paint/direct-canvas.ts +39 -0
- package/lib/paint/display-list.js +37 -12
- package/lib/paint/raster-readback.js +16 -7
- package/lib/renderer/animation-frames.js +10 -5
- package/lib/renderer/bindings.js +69 -16
- package/lib/renderer/canvas-2d.js +265 -15
- package/lib/renderer/canvas-round-rect.js +36 -0
- package/lib/renderer/css-animations.js +173 -0
- package/lib/renderer/dialog-client.js +30 -0
- package/lib/renderer/dialog-worker.js +35 -0
- package/lib/renderer/eval-environment.js +28 -0
- package/lib/renderer/file-input.js +49 -0
- package/lib/renderer/host.js +33 -4
- package/lib/renderer/html-drag.js +77 -0
- package/lib/renderer/image-loading.js +45 -2
- package/lib/renderer/main.js +4 -0
- package/lib/renderer/module-transform.js +5 -3
- package/lib/renderer/page-builtins.js +59 -0
- package/lib/renderer/page-realm.js +177 -40
- package/lib/renderer/page-renderer.js +397 -49
- package/lib/renderer/piano-input.js +3 -1
- package/lib/renderer/raster-surface.js +18 -0
- package/lib/renderer/script-rewrite.js +165 -9
- package/lib/renderer/video-presentation.js +5 -4
- package/lib/renderer/webgl/clip.js +24 -0
- package/lib/renderer/webgl/context.js +158 -0
- package/lib/renderer/webgl/cpu-fragment.js +211 -0
- package/lib/renderer/webgl/draw-cache.js +22 -0
- package/lib/renderer/webgl/pipeline.js +70 -0
- package/lib/renderer/webgl/shaders.js +58 -0
- package/lib/renderer/webgl/textures.js +43 -0
- package/lib/renderer/window-named-properties.js +46 -0
- package/lib/screenshot-cli.js +4 -7
- package/lib/style/animation-values.js +29 -0
- package/lib/style/computed-style.js +109 -13
- package/lib/style/transform.js +44 -0
- package/lib/window-size.js +10 -0
- package/package.json +1 -1
- package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
- package/patches/happy-dom/README.md +16 -0
- package/test/animation-frames.test.js +15 -0
- package/test/canvas-2d.test.js +252 -4
- package/test/canvas-frame-pacing.test.js +7 -0
- package/test/cdp-viewers.test.js +79 -0
- package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
- package/test/classic-games-patches/README.md +156 -0
- package/test/classic-games-scenarios.js +133 -0
- package/test/clipboard.test.js +16 -0
- package/test/computed-style.test.js +18 -0
- package/test/css-animation-lifecycle.test.js +28 -0
- package/test/css-animations.test.js +83 -0
- package/test/direct-canvas.test.js +39 -0
- package/test/dynamic-inline-handlers.test.js +25 -0
- package/test/eval-declarations.test.js +22 -0
- package/test/file-picker.test.js +26 -0
- package/test/fixtures/canvas-damage.js +23 -0
- package/test/fixtures/canvas-frame-pacing.js +35 -0
- package/test/fixtures/direct-canvas.js +96 -0
- package/test/fixtures/javascript-dialogs.html +8 -0
- package/test/fixtures/media-audio-lifetime.js +65 -0
- package/test/fixtures/renderer-memory.js +7 -5
- package/test/fixtures/video-black-replay.js +8 -6
- package/test/fixtures/video-slow-end.js +39 -0
- package/test/fixtures/web-audio-renderer.js +45 -0
- package/test/flex.test.js +26 -0
- package/test/grid.test.js +13 -0
- package/test/headless-shell.test.js +9 -0
- package/test/headless-window-size.test.js +28 -0
- package/test/html-rcdata.test.js +35 -0
- package/test/image-decode.test.js +18 -0
- package/test/index.test.js +1 -0
- package/test/javascript-dialog.test.js +83 -0
- package/test/manual-breakout-paddle.js +42 -0
- package/test/manual-breakout-static.js +35 -0
- package/test/manual-classic-games.js +41 -0
- package/test/manual-clumsy-bird-three-pipes.js +81 -0
- package/test/manual-clumsy-bird.js +52 -0
- package/test/manual-hextris.js +30 -0
- package/test/manual-numbered-music.js +24 -0
- package/test/manual-t-rex-runner.js +49 -0
- package/test/manual-threejs-scene.js +64 -0
- package/test/manual-webgl-benchmark.js +27 -0
- package/test/manual-webgl-cpu.html +26 -0
- package/test/media-audio-lifetime.test.js +8 -0
- package/test/mp4-frame-times.test.js +33 -0
- package/test/page-scripts.test.js +96 -1
- package/test/piano-input.test.js +15 -2
- package/test/piano.test.js +11 -2
- package/test/playback-buffer.test.js +11 -0
- package/test/pointer-drag.test.js +33 -0
- package/test/positioning.test.js +82 -0
- package/test/pulse-client.test.js +51 -2
- package/test/raster-readback.test.js +14 -0
- package/test/script-rewrite.test.js +61 -3
- package/test/video-slow-end.test.js +6 -0
- package/test/viewport-keyboard-overflow.test.js +25 -0
- package/test/wayland-win32-resources.test.js +47 -0
- package/test/wayland-win32-wire.test.js +59 -0
- package/test/web-audio-renderer.test.js +8 -0
- package/test/web-audio.test.js +264 -0
- package/test/webgl-clip.test.js +9 -0
- package/test/webgl-cpu-fragment.test.js +43 -0
- package/test/window-named-properties.test.js +41 -0
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
- package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
- package/vendor/land-renderer/LICENSE +21 -0
- package/vendor/land-renderer/README.md +14 -0
- package/vendor/land-renderer/direct-compositor.ts +443 -0
- package/vendor/land-renderer/frame.js +88 -0
- package/vendor/land-renderer/png-fast.js +85 -0
- package/vendor/land-renderer/raw-frames.js +66 -0
- package/vendor/land-renderer/render-types.ts +7 -0
- package/vendor/land-renderer/renderer.ts +134 -0
- package/vendor/land-renderer/snapshot.js +37 -0
- package/vendor/land-renderer/viewport.js +18 -0
- 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
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
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
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
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
|
@@ -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
|
+
});
|