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.
- package/AGENTS.md +4 -0
- package/README.md +74 -9
- package/ai-debug.log +5537 -0
- package/bunfig.toml +1 -0
- package/buninu-browser.js +4 -0
- package/index.js +5 -0
- package/lib/audio/piano.js +24 -1
- package/lib/audio/playback-buffer.js +27 -0
- package/lib/audio/pulse-client.js +137 -19
- package/lib/cdp-server.js +22 -11
- package/lib/happy-dom/parser.js +29 -4
- package/lib/happy-dom/selector-pseudo-adapter.js +14 -0
- package/lib/headless-shell.js +69 -19
- package/lib/input/hit-test.js +25 -7
- package/lib/layout/text-layout.js +268 -85
- package/lib/log.js +5 -2
- package/lib/media/byte-pipe.js +82 -0
- package/lib/media/position-audio.js +42 -0
- package/lib/media/video-keyframe.js +29 -0
- package/lib/network/network-service.js +161 -14
- package/lib/paint/display-list.js +316 -15
- package/lib/paint/gradient.js +11 -5
- package/lib/render-tree/index.js +19 -2
- package/lib/render-tree/svg.js +2 -1
- package/lib/renderer/bindings.js +995 -69
- package/lib/renderer/canvas-2d.js +92 -0
- package/lib/renderer/compositor.js +16 -1
- package/lib/renderer/custom-element-reactions.js +18 -0
- package/lib/renderer/history.js +42 -0
- package/lib/renderer/host.js +166 -9
- package/lib/renderer/image-loading.js +27 -0
- package/lib/renderer/interfaces.js +215 -0
- package/lib/renderer/keyboard-input.js +12 -0
- package/lib/renderer/main.js +162 -12
- package/lib/renderer/media-playback-events.js +13 -0
- package/lib/renderer/media-source.js +819 -0
- package/lib/renderer/messaging.js +411 -0
- package/lib/renderer/native-functions.js +113 -0
- package/lib/renderer/page-realm.js +1408 -242
- package/lib/renderer/page-renderer.js +1893 -223
- package/lib/renderer/piano-input.js +22 -0
- package/lib/renderer/pointer-move.js +29 -0
- package/lib/renderer/rejections.js +31 -0
- package/lib/renderer/rewrite-worker.js +22 -0
- package/lib/renderer/script-rewrite.js +805 -57
- package/lib/renderer/trusted-types.js +129 -0
- package/lib/renderer/video-presentation.js +126 -0
- package/lib/renderer/workers.js +327 -0
- package/lib/screenshot-cli.js +2 -0
- package/lib/style/computed-style.js +186 -29
- package/lib/user-agent.js +14 -2
- package/package.json +4 -1
- package/patches/happy-dom/0002-use-bun-platform-adapters.patch +139 -19
- package/patches/happy-dom/0006-parse-css-escapes-in-selectors.patch +115 -0
- package/patches/happy-dom/README.md +26 -0
- package/patches/ses/0002-embedder-native-functions-source-text-sloppy-code.patch +106 -0
- package/patches/ses/0003-cache-sloppy-evaluators.patch +47 -0
- package/patches/ses/README.md +4 -0
- package/test/canvas-2d.test.js +55 -0
- package/test/compositor.test.js +41 -1
- package/test/computed-style.test.js +59 -1
- package/test/display-list.test.js +88 -1
- package/test/download.test.js +43 -0
- package/test/fixtures/long-video.mp4 +0 -0
- package/test/fixtures/media-sync.js +57 -0
- package/test/happy-dom-platform-adapters.test.js +25 -0
- package/test/headless-shell.test.js +23 -1
- package/test/hit-test.test.js +25 -1
- package/test/media-playback-events.test.js +22 -0
- package/test/media-source.test.js +156 -0
- package/test/media-sync.test.js +12 -0
- package/test/mobile.test.js +2 -2
- package/test/navigation.test.js +61 -0
- package/test/network-service.test.js +14 -0
- package/test/page-scripts.test.js +1203 -7
- package/test/piano-input.test.js +44 -0
- package/test/playback-buffer.test.js +31 -0
- package/test/position-audio.test.js +67 -0
- package/test/positioning.test.js +95 -1
- package/test/pulse-client.test.js +84 -0
- package/test/renderer-host.test.js +5 -2
- package/test/screenshot-cli.test.js +25 -1
- package/test/script-rewrite.test.js +111 -6
- package/test/setup.js +5 -0
- package/test/text-layout.test.js +62 -0
- package/test/user-agent.test.js +60 -4
- package/test/video-presentation.test.js +67 -0
- package/vendor/happy-dom/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts +52 -22
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +18 -17
- package/vendor/ses/dist/ses.mjs +52 -6
|
@@ -32,6 +32,7 @@ const LIST_STYLE_TYPES = new Set([
|
|
|
32
32
|
"lower-alpha", "upper-alpha", "lower-latin", "upper-latin", "lower-roman", "upper-roman",
|
|
33
33
|
]);
|
|
34
34
|
const DISPLAYS = new Set([
|
|
35
|
+
"-webkit-box", "-webkit-inline-box",
|
|
35
36
|
"none", "block", "flex", "grid", "inline", "inline-block", "inline-flex", "inline-grid", "list-item",
|
|
36
37
|
"table", "inline-table", "table-row-group", "table-header-group", "table-footer-group", "table-row",
|
|
37
38
|
"table-cell", "table-caption", "table-column", "table-column-group",
|
|
@@ -50,13 +51,23 @@ const VERTICAL_ALIGNS = new Set(["baseline", "top", "middle", "bottom", "text-to
|
|
|
50
51
|
const AUTO_TRACK = Object.freeze({ min: "auto", max: "auto" });
|
|
51
52
|
const FLEX_ALIGNMENT = Object.freeze({ "flex-start": "start", "flex-end": "end", left: "start", right: "end" });
|
|
52
53
|
const GRADIENT = /^\s*(?:repeating-)?(?:linear|radial)-gradient\(/i;
|
|
54
|
+
// A background layer's image: a URL or a gradient.
|
|
55
|
+
const BACKGROUND_IMAGE = /^(?:url\(|(?:repeating-)?(?:linear|radial)-gradient\()/i;
|
|
56
|
+
const BACKGROUND_REPEATS = new Set(["repeat", "repeat-x", "repeat-y", "no-repeat", "space", "round"]);
|
|
57
|
+
const BACKGROUND_BOXES = new Set(["border-box", "padding-box", "content-box"]);
|
|
58
|
+
const BACKGROUND_ATTACHMENTS = new Set(["scroll", "fixed", "local"]);
|
|
53
59
|
const BOX_SIDES = Object.freeze(["top", "right", "bottom", "left"]);
|
|
54
60
|
const WHITE_SPACES = new Set(["normal", "nowrap", "pre", "pre-wrap"]);
|
|
55
61
|
// CSS Cascade 4 §7.3 CSS-wide keywords; revert is treated as unset (no user/UA origin split).
|
|
56
62
|
const CSS_WIDE_KEYWORDS = new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
|
|
57
63
|
// Computed-style fields a shorthand (or a property not named after its field) covers.
|
|
58
64
|
const PROPERTY_FIELDS = Object.freeze({
|
|
59
|
-
|
|
65
|
+
"-webkit-line-clamp": ["webkitLineClamp"],
|
|
66
|
+
"-webkit-box-orient": ["webkitBoxOrient"],
|
|
67
|
+
overflow: ["overflowX", "overflowY", "overflowScrollX", "overflowScrollY"],
|
|
68
|
+
"overflow-x": ["overflowX", "overflowScrollX"],
|
|
69
|
+
"overflow-y": ["overflowY", "overflowScrollY"],
|
|
70
|
+
background: ["backgroundColor", "backgroundImage", "backgroundPosition", "backgroundSize", "backgroundRepeat"],
|
|
60
71
|
flex: ["flexGrow", "flexShrink", "flexBasis"],
|
|
61
72
|
"flex-flow": ["flexDirection", "flexWrap"],
|
|
62
73
|
gap: ["rowGap", "columnGap", "gap"],
|
|
@@ -89,6 +100,10 @@ const INITIAL = Object.freeze({
|
|
|
89
100
|
colorScheme: "light",
|
|
90
101
|
backgroundColor: "rgba(0, 0, 0, 0)",
|
|
91
102
|
backgroundImage: null,
|
|
103
|
+
// Comma-separated, one entry per background-image layer (repeated as needed); null is the initial value.
|
|
104
|
+
backgroundPosition: null,
|
|
105
|
+
backgroundSize: null,
|
|
106
|
+
backgroundRepeat: null,
|
|
92
107
|
backgroundClip: "border-box",
|
|
93
108
|
boxShadow: null,
|
|
94
109
|
fontFamily: Object.freeze(["sans-serif"]),
|
|
@@ -99,6 +114,8 @@ const INITIAL = Object.freeze({
|
|
|
99
114
|
lineHeightFactor: null,
|
|
100
115
|
textAlign: "start",
|
|
101
116
|
whiteSpace: "normal",
|
|
117
|
+
webkitLineClamp: null,
|
|
118
|
+
webkitBoxOrient: "horizontal",
|
|
102
119
|
listStyleType: "disc",
|
|
103
120
|
margin: Object.freeze([0, 0, 0, 0]),
|
|
104
121
|
padding: Object.freeze([0, 0, 0, 0]),
|
|
@@ -133,6 +150,8 @@ const INITIAL = Object.freeze({
|
|
|
133
150
|
inset: Object.freeze(["auto", "auto", "auto", "auto"]),
|
|
134
151
|
zIndex: "auto",
|
|
135
152
|
transform: null,
|
|
153
|
+
// Raw text (default "50% 50%"); paint resolves it against the border box.
|
|
154
|
+
transformOrigin: null,
|
|
136
155
|
justifyItems: "normal",
|
|
137
156
|
justifySelf: "auto",
|
|
138
157
|
gridTemplateColumns: null,
|
|
@@ -161,6 +180,10 @@ const INITIAL = Object.freeze({
|
|
|
161
180
|
opacity: 1,
|
|
162
181
|
overflowX: "visible",
|
|
163
182
|
overflowY: "visible",
|
|
183
|
+
// Scroll containers (CSS Overflow 3 §3): "auto" for auto/scroll/overlay,
|
|
184
|
+
// which the user scrolls; "hidden", which only scripts scroll; else null.
|
|
185
|
+
overflowScrollX: null,
|
|
186
|
+
overflowScrollY: null,
|
|
164
187
|
textOverflow: "clip",
|
|
165
188
|
clip: null,
|
|
166
189
|
clipPath: null,
|
|
@@ -187,6 +210,7 @@ const UA_DECLARATIONS = new Map(Object.entries({
|
|
|
187
210
|
FIGURE: "margin: 1em 40px",
|
|
188
211
|
PRE: "margin: 1em 0; font-family: monospace",
|
|
189
212
|
CODE: "font-family: monospace",
|
|
213
|
+
IFRAME: "border: 2px inset",
|
|
190
214
|
KBD: "font-family: monospace",
|
|
191
215
|
SAMP: "font-family: monospace",
|
|
192
216
|
TT: "font-family: monospace",
|
|
@@ -237,6 +261,8 @@ export function controlKind(element) {
|
|
|
237
261
|
}
|
|
238
262
|
|
|
239
263
|
/** HTML §15.3.4: :link (visited links are not tracked). */
|
|
264
|
+
// <iframe frameborder="0"> (or "no"): no border in place of the default one.
|
|
265
|
+
const UA_FRAMELESS_DECLARATIONS = parseDeclarations("border: none");
|
|
240
266
|
const UA_LINK_DECLARATIONS = parseDeclarations("color: #0000EE; text-decoration: underline; cursor: pointer");
|
|
241
267
|
const DECORATION_LINES = new Set(["underline", "overline", "line-through"]);
|
|
242
268
|
const DECORATION_STYLES = new Set(["solid", "double", "dotted", "dashed", "wavy"]);
|
|
@@ -422,6 +448,11 @@ export class StyleEngine {
|
|
|
422
448
|
return this.#styles.get(element) ?? INITIAL;
|
|
423
449
|
}
|
|
424
450
|
|
|
451
|
+
/** An element's custom properties (--*), name to value text, inherited ones included. */
|
|
452
|
+
customPropertiesOf(element) {
|
|
453
|
+
return this.#customProperties.get(element) ?? EMPTY_MAP;
|
|
454
|
+
}
|
|
455
|
+
|
|
425
456
|
/** The computed style of an element's ::before/::after box, or null when it generates none. */
|
|
426
457
|
getPseudo(element, pseudo) {
|
|
427
458
|
return this.#pseudoStyles.get(element)?.[pseudo] ?? null;
|
|
@@ -618,7 +649,9 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
618
649
|
const isLink = (element.tagName === "A" || element.tagName === "AREA") && element.hasAttribute("href");
|
|
619
650
|
const control = controlKind(element);
|
|
620
651
|
if (control === "hidden") values.display = "none";
|
|
621
|
-
|
|
652
|
+
const frameless = element.tagName === "IFRAME" && ["0", "no"].includes(element.getAttribute("frameborder")?.trim().toLowerCase());
|
|
653
|
+
const uaDeclarations = frameless ? UA_FRAMELESS_DECLARATIONS : UA_DECLARATIONS.get(element.tagName);
|
|
654
|
+
for (const declaration of [...(uaDeclarations?.values() ?? []),
|
|
622
655
|
...(isLink ? UA_LINK_DECLARATIONS.values() : []), ...(UA_CONTROL_DECLARATIONS[control]?.values() ?? [])]) {
|
|
623
656
|
if (!declarations.has(declaration.name)) {
|
|
624
657
|
declarations.set(declaration.name, { ...declaration, specificity: -1, order: -1, layer: -Infinity });
|
|
@@ -1062,6 +1095,13 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1062
1095
|
break;
|
|
1063
1096
|
}
|
|
1064
1097
|
case "display": if (DISPLAYS.has(value)) style.display = value; break;
|
|
1098
|
+
case "-webkit-line-clamp":
|
|
1099
|
+
if (value === "none") style.webkitLineClamp = null;
|
|
1100
|
+
else if (/^[1-9]\d*$/.test(value)) style.webkitLineClamp = Number(value);
|
|
1101
|
+
break;
|
|
1102
|
+
case "-webkit-box-orient":
|
|
1103
|
+
if (["vertical", "horizontal"].includes(value)) style.webkitBoxOrient = value;
|
|
1104
|
+
break;
|
|
1065
1105
|
case "text-decoration-line": {
|
|
1066
1106
|
const lines = parseDecorationLines(value);
|
|
1067
1107
|
if (lines) style.textDecorationLine = lines;
|
|
@@ -1112,13 +1152,22 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1112
1152
|
case "overflow":
|
|
1113
1153
|
case "overflow-x":
|
|
1114
1154
|
case "overflow-y": {
|
|
1115
|
-
//
|
|
1155
|
+
// Every non-visible value clips; all but clip also make a scroll container.
|
|
1116
1156
|
const parts = value.split(/\s+/);
|
|
1117
1157
|
const clips = (part) => (["hidden", "clip", "auto", "scroll", "overlay"].includes(part) ? "clip" : part === "visible" ? "visible" : null);
|
|
1118
|
-
const
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1158
|
+
const scrolls = (part) => (["auto", "scroll", "overlay"].includes(part) ? "auto" : part === "hidden" ? "hidden" : null);
|
|
1159
|
+
const xPart = parts[0];
|
|
1160
|
+
const yPart = name === "overflow" ? parts[1] ?? parts[0] : parts[0];
|
|
1161
|
+
const x = clips(xPart);
|
|
1162
|
+
const y = clips(yPart);
|
|
1163
|
+
if (name !== "overflow-y" && x) {
|
|
1164
|
+
style.overflowX = x;
|
|
1165
|
+
style.overflowScrollX = scrolls(xPart);
|
|
1166
|
+
}
|
|
1167
|
+
if (name !== "overflow-x" && y) {
|
|
1168
|
+
style.overflowY = y;
|
|
1169
|
+
style.overflowScrollY = scrolls(yPart);
|
|
1170
|
+
}
|
|
1122
1171
|
break;
|
|
1123
1172
|
}
|
|
1124
1173
|
case "text-overflow":
|
|
@@ -1157,24 +1206,37 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1157
1206
|
break;
|
|
1158
1207
|
}
|
|
1159
1208
|
case "background": {
|
|
1160
|
-
// The shorthand resets
|
|
1161
|
-
const layers = splitTopLevel(value, ",");
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
.
|
|
1165
|
-
.
|
|
1166
|
-
|
|
1167
|
-
style.backgroundImage =
|
|
1209
|
+
// The shorthand resets the longhands; only the final layer may carry a color.
|
|
1210
|
+
const layers = splitTopLevel(value, ",").map(parseBackgroundLayer);
|
|
1211
|
+
if (layers.some((layer) => !layer)) break;
|
|
1212
|
+
style.backgroundColor = layers.at(-1).color
|
|
1213
|
+
? resolvedColor(layers.at(-1).color, style.colorScheme) ?? INITIAL.backgroundColor
|
|
1214
|
+
: INITIAL.backgroundColor;
|
|
1215
|
+
const hasImage = layers.some((layer) => layer.image !== "none");
|
|
1216
|
+
style.backgroundImage = hasImage ? layers.map((layer) => layer.image).join(", ") : null;
|
|
1217
|
+
const list = (field, initial) => layers.some((layer) => layer[field] !== initial)
|
|
1218
|
+
? layers.map((layer) => layer[field]).join(", ")
|
|
1219
|
+
: null;
|
|
1220
|
+
style.backgroundPosition = hasImage ? list("position", "0% 0%") : null;
|
|
1221
|
+
style.backgroundSize = hasImage ? list("size", "auto") : null;
|
|
1222
|
+
style.backgroundRepeat = hasImage ? list("repeat", "repeat") : null;
|
|
1168
1223
|
break;
|
|
1169
1224
|
}
|
|
1170
1225
|
case "background-image": {
|
|
1171
1226
|
if (value === "none") style.backgroundImage = null;
|
|
1172
1227
|
else {
|
|
1173
|
-
const images = splitTopLevel(value, ",").
|
|
1174
|
-
if (images.
|
|
1228
|
+
const images = splitTopLevel(value, ",").map((layer) => BACKGROUND_IMAGE.test(layer) || layer === "none" ? layer : null);
|
|
1229
|
+
if (images.every(Boolean) && images.some((layer) => layer !== "none")) style.backgroundImage = images.join(", ");
|
|
1175
1230
|
}
|
|
1176
1231
|
break;
|
|
1177
1232
|
}
|
|
1233
|
+
case "background-position":
|
|
1234
|
+
case "background-size":
|
|
1235
|
+
case "background-repeat": {
|
|
1236
|
+
const field = { "background-position": "backgroundPosition", "background-size": "backgroundSize", "background-repeat": "backgroundRepeat" }[name];
|
|
1237
|
+
style[field] = value.toLowerCase().replace(/\s+/g, " ");
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1178
1240
|
case "box-shadow": {
|
|
1179
1241
|
const shadows = value === "none" ? null : parseBoxShadows(value, style.fontSize);
|
|
1180
1242
|
if (value === "none" || shadows) style.boxShadow = shadows;
|
|
@@ -1186,8 +1248,11 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1186
1248
|
break;
|
|
1187
1249
|
case "font-size": {
|
|
1188
1250
|
const parentSize = parentStyle?.fontSize ?? INITIAL.fontSize;
|
|
1251
|
+
const specified = computedLength(value, parentSize, true);
|
|
1252
|
+
const relativeSize = specified?.unit === "%" ? parentSize * specified.value / 100
|
|
1253
|
+
: specified?.unit === "math" && specified.kind === "calc" ? specified.px + parentSize * specified.percent / 100 : specified;
|
|
1189
1254
|
const length = FONT_SIZE_KEYWORDS[value] ?? (value === "larger" ? parentSize * 1.2 : value === "smaller" ? parentSize / 1.2
|
|
1190
|
-
:
|
|
1255
|
+
: relativeSize);
|
|
1191
1256
|
if (length > 0) style.fontSize = length;
|
|
1192
1257
|
break;
|
|
1193
1258
|
}
|
|
@@ -1229,10 +1294,13 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1229
1294
|
break;
|
|
1230
1295
|
}
|
|
1231
1296
|
case "transform": {
|
|
1232
|
-
const transform = value === "none" ? null :
|
|
1297
|
+
const transform = value === "none" ? null : parseTransform(value, style.fontSize);
|
|
1233
1298
|
if (transform !== undefined) style.transform = transform;
|
|
1234
1299
|
break;
|
|
1235
1300
|
}
|
|
1301
|
+
case "transform-origin":
|
|
1302
|
+
style.transformOrigin = value.toLowerCase().replace(/\s+/g, " ");
|
|
1303
|
+
break;
|
|
1236
1304
|
case "content": {
|
|
1237
1305
|
const content = parseContent(value);
|
|
1238
1306
|
if (content !== undefined) style.content = content;
|
|
@@ -1296,12 +1364,15 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1296
1364
|
if (value === "normal") {
|
|
1297
1365
|
style.lineHeight = null;
|
|
1298
1366
|
style.lineHeightFactor = null;
|
|
1299
|
-
} else if (Number.isFinite(numeric) && numeric
|
|
1367
|
+
} else if (value !== "" && Number.isFinite(numeric) && numeric >= 0) {
|
|
1300
1368
|
style.lineHeight = numeric * style.fontSize;
|
|
1301
1369
|
style.lineHeightFactor = numeric;
|
|
1302
1370
|
} else {
|
|
1303
|
-
|
|
1304
|
-
|
|
1371
|
+
// Non-negative, so 0 is valid (icon buttons set line-height: 0); a
|
|
1372
|
+
// percentage computes to a length against this element's font size.
|
|
1373
|
+
const percentage = /^(\d+\.?\d*|\.\d+)%$/.exec(value);
|
|
1374
|
+
const length = percentage ? Number(percentage[1]) / 100 * style.fontSize : computedLength(value, style.fontSize);
|
|
1375
|
+
if (length >= 0) {
|
|
1305
1376
|
style.lineHeight = length;
|
|
1306
1377
|
style.lineHeightFactor = null;
|
|
1307
1378
|
}
|
|
@@ -1314,7 +1385,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1314
1385
|
break;
|
|
1315
1386
|
}
|
|
1316
1387
|
case "padding": {
|
|
1317
|
-
const box = boxLengths(value, false, style.fontSize, false);
|
|
1388
|
+
const box = boxLengths(value, false, style.fontSize, false, true);
|
|
1318
1389
|
if (box) style.padding = box;
|
|
1319
1390
|
break;
|
|
1320
1391
|
}
|
|
@@ -1328,8 +1399,9 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1328
1399
|
case "padding-left": {
|
|
1329
1400
|
const [box, side] = name.split("-");
|
|
1330
1401
|
const isMargin = box === "margin";
|
|
1331
|
-
const length = isMargin && value === "auto" ? "auto" : computedLength(value, style.fontSize);
|
|
1332
|
-
if (length === "auto" || (
|
|
1402
|
+
const length = isMargin && value === "auto" ? "auto" : computedLength(value, style.fontSize, !isMargin);
|
|
1403
|
+
if (length === "auto" || (!isMargin && isLength(length) && typeof length === "object"
|
|
1404
|
+
&& (!isPercentage(length) || length.value >= 0)) || (Number.isFinite(length) && (isMargin || length >= 0))) {
|
|
1333
1405
|
style[box] = style[box].with(BOX_SIDES.indexOf(side), length);
|
|
1334
1406
|
}
|
|
1335
1407
|
break;
|
|
@@ -1878,6 +1950,32 @@ function pseudoElement(pseudo) {
|
|
|
1878
1950
|
* percentages of the element's own border box). Other functions are ignored
|
|
1879
1951
|
* until transforms are painted with matrices.
|
|
1880
1952
|
*/
|
|
1953
|
+
/**
|
|
1954
|
+
* The translation (layout moves the box) and scale (paint scales it about
|
|
1955
|
+
* transform-origin) of a transform list; other functions are ignored.
|
|
1956
|
+
*/
|
|
1957
|
+
function parseTransform(value, fontSize) {
|
|
1958
|
+
const translation = parseTranslate(value, fontSize);
|
|
1959
|
+
if (translation === undefined) return undefined;
|
|
1960
|
+
let scaleX = 1;
|
|
1961
|
+
let scaleY = 1;
|
|
1962
|
+
let found = false;
|
|
1963
|
+
for (const match of value.matchAll(/\b(scale|scaleX|scaleY|scale3d)\(([^)]*)\)/gi)) {
|
|
1964
|
+
const args = splitTopLevel(match[2], ",").map((part) => part.endsWith("%") ? Number(part.slice(0, -1)) / 100 : Number(part));
|
|
1965
|
+
if (!args.length || args.some((part) => !Number.isFinite(part))) return undefined;
|
|
1966
|
+
const kind = match[1].toLowerCase();
|
|
1967
|
+
if (kind === "scalex") scaleX *= args[0];
|
|
1968
|
+
else if (kind === "scaley") scaleY *= args[0];
|
|
1969
|
+
else {
|
|
1970
|
+
scaleX *= args[0];
|
|
1971
|
+
scaleY *= args[1] ?? args[0];
|
|
1972
|
+
}
|
|
1973
|
+
found = true;
|
|
1974
|
+
}
|
|
1975
|
+
if (!found || (scaleX === 1 && scaleY === 1)) return translation;
|
|
1976
|
+
return Object.freeze({ x: translation?.x ?? 0, y: translation?.y ?? 0, scaleX, scaleY });
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1881
1979
|
function parseTranslate(value, fontSize) {
|
|
1882
1980
|
let x = 0;
|
|
1883
1981
|
let y = 0;
|
|
@@ -1916,7 +2014,7 @@ function parseContent(value) {
|
|
|
1916
2014
|
* undefined property without a fallback leaves nothing (the declaration then
|
|
1917
2015
|
* has no effect).
|
|
1918
2016
|
*/
|
|
1919
|
-
function resolveVariables(value, customProperties, seen = new Set()) {
|
|
2017
|
+
export function resolveVariables(value, customProperties, seen = new Set()) {
|
|
1920
2018
|
const text = String(value);
|
|
1921
2019
|
// Most values use no custom properties; skip the parsing.
|
|
1922
2020
|
if (!text.includes("var(")) return text.trim();
|
|
@@ -2317,6 +2415,64 @@ function resolvedColor(value, scheme) {
|
|
|
2317
2415
|
return /^rgba\(/.test(color ?? "") ? color : null;
|
|
2318
2416
|
}
|
|
2319
2417
|
|
|
2418
|
+
/**
|
|
2419
|
+
* One layer of the background shorthand, its parts as text: the image, the
|
|
2420
|
+
* position (before a slash) and size (after it), the repeat style and a color.
|
|
2421
|
+
* Null when a token fits none of them.
|
|
2422
|
+
*/
|
|
2423
|
+
function parseBackgroundLayer(layer) {
|
|
2424
|
+
const tokens = [];
|
|
2425
|
+
let depth = 0;
|
|
2426
|
+
let token = "";
|
|
2427
|
+
for (const character of layer) {
|
|
2428
|
+
if (depth === 0 && (/\s/.test(character) || character === "/")) {
|
|
2429
|
+
if (token) tokens.push(token);
|
|
2430
|
+
if (character === "/") tokens.push("/");
|
|
2431
|
+
token = "";
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
if (character === "(") depth++;
|
|
2435
|
+
else if (character === ")") depth--;
|
|
2436
|
+
token += character;
|
|
2437
|
+
}
|
|
2438
|
+
if (token) tokens.push(token);
|
|
2439
|
+
const result = { image: "none", position: "0% 0%", size: "auto", repeat: "repeat", color: null };
|
|
2440
|
+
const position = [];
|
|
2441
|
+
const size = [];
|
|
2442
|
+
const repeat = [];
|
|
2443
|
+
let afterSlash = false;
|
|
2444
|
+
for (const raw of tokens) {
|
|
2445
|
+
const lower = raw.toLowerCase();
|
|
2446
|
+
if (raw === "/") afterSlash = true;
|
|
2447
|
+
else if (BACKGROUND_IMAGE.test(raw)) result.image = raw;
|
|
2448
|
+
else if (lower === "none") result.image = "none";
|
|
2449
|
+
else if (BACKGROUND_REPEATS.has(lower)) repeat.push(lower);
|
|
2450
|
+
else if (BACKGROUND_BOXES.has(lower) || BACKGROUND_ATTACHMENTS.has(lower)) continue;
|
|
2451
|
+
else if (afterSlash && (lower === "cover" || lower === "contain" || lower === "auto" || /^-?[\d.]/.test(lower) || lower.startsWith("calc("))) size.push(lower);
|
|
2452
|
+
else if (["left", "right", "top", "bottom", "center"].includes(lower) || /^-?[\d.]/.test(lower) || lower.startsWith("calc(")) {
|
|
2453
|
+
afterSlash = false;
|
|
2454
|
+
position.push(lower);
|
|
2455
|
+
} else if (resolvedColor(raw, "light")) result.color = raw;
|
|
2456
|
+
else return null;
|
|
2457
|
+
}
|
|
2458
|
+
if (position.length) result.position = position.join(" ");
|
|
2459
|
+
if (size.length) result.size = size.join(" ");
|
|
2460
|
+
if (repeat.length) result.repeat = repeat.join(" ");
|
|
2461
|
+
return result;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
/** A style sheet with each relative url() made absolute against the sheet's own URL. */
|
|
2465
|
+
export function absolutizeStyleSheetUrls(text, base) {
|
|
2466
|
+
return text.replace(/url\(\s*(["']?)([^"')]*)\1\s*\)/gi, (whole, quote, url) => {
|
|
2467
|
+
if (!url || /^(?:[a-z][a-z\d+.-]*:|#)/i.test(url)) return whole;
|
|
2468
|
+
try {
|
|
2469
|
+
return `url(${quote}${new URL(url, base).href}${quote})`;
|
|
2470
|
+
} catch {
|
|
2471
|
+
return whole;
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2320
2476
|
function splitTopLevel(source, separator) {
|
|
2321
2477
|
const parts = [];
|
|
2322
2478
|
let depth = 0;
|
|
@@ -2325,7 +2481,7 @@ function splitTopLevel(source, separator) {
|
|
|
2325
2481
|
const character = source[index];
|
|
2326
2482
|
if (character === "(") depth++;
|
|
2327
2483
|
else if (character === ")") depth--;
|
|
2328
|
-
else if (character === separator && depth === 0) {
|
|
2484
|
+
else if ((character === separator || separator === " " && /\s/.test(character)) && depth === 0) {
|
|
2329
2485
|
parts.push(source.slice(start, index).trim());
|
|
2330
2486
|
start = index + 1;
|
|
2331
2487
|
}
|
|
@@ -2335,10 +2491,11 @@ function splitTopLevel(source, separator) {
|
|
|
2335
2491
|
}
|
|
2336
2492
|
|
|
2337
2493
|
function boxLengths(value, allowNegative, emSize, allowAuto, allowPercentage = false) {
|
|
2338
|
-
const parts = value.trim().
|
|
2494
|
+
const parts = splitTopLevel(value.trim(), " ").filter(Boolean).map((part) =>
|
|
2339
2495
|
allowAuto && part === "auto" ? "auto" : computedLength(part, emSize, allowPercentage));
|
|
2340
2496
|
if (parts.length < 1 || parts.length > 4 || parts.some((part) =>
|
|
2341
|
-
part !== "auto" && !
|
|
2497
|
+
part !== "auto" && !(allowPercentage && isLength(part) && typeof part === "object"
|
|
2498
|
+
&& (allowNegative || !isPercentage(part) || part.value >= 0)) && (!Number.isFinite(part) || (!allowNegative && part < 0)))) return null;
|
|
2342
2499
|
const [top, right = top, bottom = top, left = right] = parts.length === 3
|
|
2343
2500
|
? [parts[0], parts[1], parts[2], parts[1]]
|
|
2344
2501
|
: parts.length === 2
|
package/lib/user-agent.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import packageInfo from "../package.json" with { type: "json" };
|
|
2
|
+
|
|
3
|
+
export const BROWSER_PRODUCT = `Buninu/${packageInfo.version}`;
|
|
4
|
+
// Compatibility identities, not the version of this engine. Bun runs
|
|
5
|
+
// JavaScriptCore, so a WebKit profile can avoid a V8/JavaScriptCore mismatch.
|
|
6
|
+
// Keep the complete profile together: changing just the UA leaves conflicting
|
|
7
|
+
// vendor and User-Agent Client Hints exposed to scripts.
|
|
8
|
+
export const WEBKIT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15";
|
|
9
|
+
// Chrome 154 compatibility profile (2026-09-22).
|
|
3
10
|
export const CHROMIUM_VERSION = "154.0.8037.57";
|
|
4
11
|
|
|
5
12
|
export function desktopUserAgent(platform = process.platform, arch = process.arch) {
|
|
@@ -10,3 +17,8 @@ export function desktopUserAgent(platform = process.platform, arch = process.arc
|
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
export const DESKTOP_USER_AGENT = desktopUserAgent();
|
|
20
|
+
|
|
21
|
+
/** Desktop compatibility flags are last-wins; mobile presentation takes priority. */
|
|
22
|
+
export function webkitFromArgs(args) {
|
|
23
|
+
return !args.includes("--mobile") && args.lastIndexOf("--webkit") >= args.lastIndexOf("--chromium");
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "buninu-browser",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Buninu Browser is a browser engine written in JavaScript and WebAssembly for Bun.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"buninu",
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
"bugs": {
|
|
33
33
|
"url": "https://github.com/jjtseng93/buninu-browser/issues"
|
|
34
34
|
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "bun test --parallel"
|
|
37
|
+
},
|
|
35
38
|
"license": "MIT",
|
|
36
39
|
"engines": {
|
|
37
40
|
"bun": ">=1.4.2"
|
|
@@ -9,7 +9,7 @@ index 2de8270..0c5034b 100644
|
|
|
9
9
|
import ProgressEvent from '../event/events/ProgressEvent.js';
|
|
10
10
|
import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js';
|
|
11
11
|
diff --git a/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts b/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts
|
|
12
|
-
index f2c85d6..
|
|
12
|
+
index f2c85d6..dcef8e0 100644
|
|
13
13
|
--- a/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts
|
|
14
14
|
+++ b/packages/happy-dom/src/nodes/html-image-element/HTMLImageElement.ts
|
|
15
15
|
@@ -4,7 +4,7 @@ import type ICanvasShape from '../../canvas/ICanvasShape.js';
|
|
@@ -21,39 +21,159 @@ index f2c85d6..961edb1 100644
|
|
|
21
21
|
import type Response from '../../fetch/Response.js';
|
|
22
22
|
import type Attr from '../attr/Attr.js';
|
|
23
23
|
|
|
24
|
-
@@ -
|
|
25
|
-
|
|
24
|
+
@@ -19,6 +19,8 @@ const DATA_URL_REGEX = /^\s*data:([^,]+)?,(.*)/;
|
|
25
|
+
export default class HTMLImageElement extends HTMLElement implements ICanvasShape {
|
|
26
|
+
public [PropertySymbol.tagName] = 'IMG';
|
|
27
|
+
public [PropertySymbol.complete] = true;
|
|
28
|
+
+ // Bumped whenever src changes: an image decoded for an earlier src is dropped.
|
|
29
|
+
+ #loadGeneration = 0;
|
|
30
|
+
public [PropertySymbol.naturalHeight] = 0;
|
|
31
|
+
public [PropertySymbol.naturalWidth] = 0;
|
|
32
|
+
public [PropertySymbol.loading] = 'auto';
|
|
33
|
+
@@ -357,6 +359,7 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
34
|
+
super[PropertySymbol.onSetAttribute](attribute, replacedAttribute);
|
|
35
|
+
|
|
36
|
+
if (attribute[PropertySymbol.name] === 'src') {
|
|
37
|
+
+ const generation = ++this.#loadGeneration;
|
|
38
|
+
const url = this.#parseUrl(attribute[PropertySymbol.value]);
|
|
39
|
+
|
|
40
|
+
if (!url) {
|
|
41
|
+
@@ -367,13 +370,13 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- if (this.#loadDataUrl(url)) {
|
|
46
|
+
+ if (this.#loadDataUrl(url, generation)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const settings = new WindowBrowserContext(this[PropertySymbol.window]).getSettings();
|
|
51
|
+
if (settings?.enableImageFileLoading) {
|
|
52
|
+
- this.#loadUrlSource(this.src);
|
|
53
|
+
+ this.#loadUrlSource(this.src, generation);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
@@ -385,6 +388,7 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
58
|
+
super[PropertySymbol.onRemoveAttribute](removedAttribute);
|
|
59
|
+
|
|
60
|
+
if (removedAttribute[PropertySymbol.name] === 'src') {
|
|
61
|
+
+ this.#loadGeneration++;
|
|
26
62
|
this[PropertySymbol.complete] = true;
|
|
63
|
+
this[PropertySymbol.naturalHeight] = 0;
|
|
64
|
+
this[PropertySymbol.naturalWidth] = 0;
|
|
65
|
+
@@ -415,24 +419,37 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
66
|
+
* Loads the image from the given data URL.
|
|
67
|
+
*
|
|
68
|
+
* @param src Source.
|
|
69
|
+
+ * @param generation The load this is (see #loadGeneration).
|
|
70
|
+
* @returns True if the image was loaded, false otherwise.
|
|
71
|
+
*/
|
|
72
|
+
- #loadDataUrl(src: string): boolean {
|
|
73
|
+
+ #loadDataUrl(src: string, generation: number): boolean {
|
|
74
|
+
const dataUrlMatch = src.match(DATA_URL_REGEX);
|
|
75
|
+
|
|
76
|
+
if (dataUrlMatch) {
|
|
77
|
+
const buffer = Buffer.from(dataUrlMatch[2], 'base64');
|
|
78
|
+
- this[PropertySymbol.complete] = true;
|
|
27
79
|
this[PropertySymbol.buffer] = buffer;
|
|
28
80
|
- try {
|
|
29
81
|
- const dimensions = BufferImageSize(buffer);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this[PropertySymbol.naturalHeight] = dimensions.height;
|
|
33
|
-
this[PropertySymbol.naturalWidth] = dimensions.width;
|
|
82
|
+
- this[PropertySymbol.naturalHeight] = dimensions.height;
|
|
83
|
+
- this[PropertySymbol.naturalWidth] = dimensions.width;
|
|
34
84
|
- } catch (e) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
+ }).catch(() => {
|
|
38
|
-
this[PropertySymbol.naturalHeight] = 0;
|
|
39
|
-
this[PropertySymbol.naturalWidth] = 0;
|
|
85
|
+
- this[PropertySymbol.naturalHeight] = 0;
|
|
86
|
+
- this[PropertySymbol.naturalWidth] = 0;
|
|
40
87
|
- }
|
|
41
88
|
- this.dispatchEvent(new this[PropertySymbol.window].Event('load'));
|
|
42
|
-
+
|
|
43
|
-
+
|
|
44
|
-
+
|
|
89
|
+
+ this[PropertySymbol.complete] = false;
|
|
90
|
+
+ // The size is read asynchronously; a src set meanwhile wins.
|
|
91
|
+
+ ImageSize(buffer).then(
|
|
92
|
+
+ (dimensions) => {
|
|
93
|
+
+ if (generation !== this.#loadGeneration) {
|
|
94
|
+
+ return;
|
|
95
|
+
+ }
|
|
96
|
+
+ this[PropertySymbol.naturalHeight] = dimensions.height;
|
|
97
|
+
+ this[PropertySymbol.naturalWidth] = dimensions.width;
|
|
98
|
+
+ this[PropertySymbol.complete] = true;
|
|
99
|
+
+ this.dispatchEvent(new this[PropertySymbol.window].Event('load'));
|
|
100
|
+
+ },
|
|
101
|
+
+ () => {
|
|
102
|
+
+ if (generation !== this.#loadGeneration) {
|
|
103
|
+
+ return;
|
|
104
|
+
+ }
|
|
105
|
+
+ this[PropertySymbol.naturalHeight] = 0;
|
|
106
|
+
+ this[PropertySymbol.naturalWidth] = 0;
|
|
107
|
+
+ this[PropertySymbol.complete] = true;
|
|
108
|
+
+ this.dispatchEvent(new this[PropertySymbol.window].Event('error'));
|
|
109
|
+
+ }
|
|
110
|
+
+ );
|
|
45
111
|
return true;
|
|
46
112
|
}
|
|
47
113
|
return false;
|
|
48
|
-
@@ -
|
|
114
|
+
@@ -442,8 +459,9 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
115
|
+
* Loads the image from the given source.
|
|
116
|
+
*
|
|
117
|
+
* @param src Source.
|
|
118
|
+
+ * @param generation The load this is (see #loadGeneration).
|
|
119
|
+
*/
|
|
120
|
+
- async #loadUrlSource(src: string): Promise<void> {
|
|
121
|
+
+ async #loadUrlSource(src: string, generation: number): Promise<void> {
|
|
122
|
+
this[PropertySymbol.complete] = false;
|
|
123
|
+
this[PropertySymbol.naturalHeight] = 0;
|
|
124
|
+
this[PropertySymbol.naturalWidth] = 0;
|
|
125
|
+
@@ -457,6 +475,10 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
126
|
+
error = <Error>e;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
+ if (generation !== this.#loadGeneration) {
|
|
130
|
+
+ return;
|
|
131
|
+
+ }
|
|
132
|
+
+
|
|
133
|
+
if (error || !response?.ok) {
|
|
134
|
+
this[PropertySymbol.complete] = true;
|
|
135
|
+
this.dispatchEvent(new this[PropertySymbol.window].Event('error'));
|
|
136
|
+
@@ -475,23 +497,34 @@ export default class HTMLImageElement extends HTMLElement implements ICanvasShap
|
|
137
|
+
try {
|
|
138
|
+
buffer = await response.buffer();
|
|
139
|
+
} catch (e) {
|
|
140
|
+
+ if (generation !== this.#loadGeneration) {
|
|
141
|
+
+ return;
|
|
142
|
+
+ }
|
|
143
|
+
this[PropertySymbol.complete] = true;
|
|
144
|
+
this.dispatchEvent(new this[PropertySymbol.window].Event('error'));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
- this[PropertySymbol.complete] = true;
|
|
149
|
+
+ if (generation !== this.#loadGeneration) {
|
|
150
|
+
+ return;
|
|
151
|
+
+ }
|
|
152
|
+
+
|
|
49
153
|
this[PropertySymbol.buffer] = buffer;
|
|
50
154
|
|
|
155
|
+
+ let dimensions = { width: 0, height: 0 };
|
|
51
156
|
try {
|
|
52
157
|
- const dimensions = BufferImageSize(buffer);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
158
|
+
- this[PropertySymbol.naturalHeight] = dimensions.height;
|
|
159
|
+
- this[PropertySymbol.naturalWidth] = dimensions.width;
|
|
160
|
+
+ dimensions = await ImageSize(buffer);
|
|
56
161
|
} catch (e) {
|
|
162
|
+
- this[PropertySymbol.naturalHeight] = 0;
|
|
163
|
+
- this[PropertySymbol.naturalWidth] = 0;
|
|
164
|
+
+ // Undecodable: no natural size.
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
+ if (generation !== this.#loadGeneration) {
|
|
168
|
+
+ return;
|
|
169
|
+
+ }
|
|
170
|
+
+
|
|
171
|
+
+ this[PropertySymbol.naturalHeight] = dimensions.height;
|
|
172
|
+
+ this[PropertySymbol.naturalWidth] = dimensions.width;
|
|
173
|
+
+ this[PropertySymbol.complete] = true;
|
|
174
|
+
this.dispatchEvent(new this[PropertySymbol.window].Event('load'));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
57
177
|
diff --git a/packages/happy-dom/src/web-socket/WebSocket.ts b/packages/happy-dom/src/web-socket/WebSocket.ts
|
|
58
178
|
index 3075e3a..bea5f97 100644
|
|
59
179
|
--- a/packages/happy-dom/src/web-socket/WebSocket.ts
|