buninu-browser 0.2.1 → 0.2.2
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/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/pulse-client.js +120 -12
- package/lib/cdp-server.js +16 -11
- package/lib/happy-dom/parser.js +24 -4
- package/lib/headless-shell.js +66 -19
- package/lib/input/hit-test.js +25 -7
- package/lib/layout/text-layout.js +196 -49
- package/lib/log.js +5 -2
- package/lib/media/byte-pipe.js +82 -0
- package/lib/media/position-audio.js +42 -0
- package/lib/network/network-service.js +161 -14
- package/lib/paint/display-list.js +300 -14
- package/lib/paint/gradient.js +11 -5
- package/lib/render-tree/index.js +17 -2
- package/lib/render-tree/svg.js +2 -1
- package/lib/renderer/bindings.js +985 -68
- package/lib/renderer/canvas-2d.js +92 -0
- package/lib/renderer/host.js +158 -8
- package/lib/renderer/interfaces.js +215 -0
- package/lib/renderer/main.js +160 -12
- 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 +1405 -198
- package/lib/renderer/page-renderer.js +1736 -193
- 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/workers.js +327 -0
- package/lib/screenshot-cli.js +2 -0
- package/lib/style/computed-style.js +162 -22
- 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 +3 -1
- package/test/computed-style.test.js +38 -1
- package/test/display-list.test.js +74 -1
- package/test/download.test.js +35 -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 +1 -1
- package/test/hit-test.test.js +25 -1
- 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 +1045 -7
- package/test/position-audio.test.js +67 -0
- package/test/positioning.test.js +95 -1
- package/test/pulse-client.test.js +69 -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/user-agent.test.js +60 -4
- 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
|
@@ -50,13 +50,21 @@ const VERTICAL_ALIGNS = new Set(["baseline", "top", "middle", "bottom", "text-to
|
|
|
50
50
|
const AUTO_TRACK = Object.freeze({ min: "auto", max: "auto" });
|
|
51
51
|
const FLEX_ALIGNMENT = Object.freeze({ "flex-start": "start", "flex-end": "end", left: "start", right: "end" });
|
|
52
52
|
const GRADIENT = /^\s*(?:repeating-)?(?:linear|radial)-gradient\(/i;
|
|
53
|
+
// A background layer's image: a URL or a gradient.
|
|
54
|
+
const BACKGROUND_IMAGE = /^(?:url\(|(?:repeating-)?(?:linear|radial)-gradient\()/i;
|
|
55
|
+
const BACKGROUND_REPEATS = new Set(["repeat", "repeat-x", "repeat-y", "no-repeat", "space", "round"]);
|
|
56
|
+
const BACKGROUND_BOXES = new Set(["border-box", "padding-box", "content-box"]);
|
|
57
|
+
const BACKGROUND_ATTACHMENTS = new Set(["scroll", "fixed", "local"]);
|
|
53
58
|
const BOX_SIDES = Object.freeze(["top", "right", "bottom", "left"]);
|
|
54
59
|
const WHITE_SPACES = new Set(["normal", "nowrap", "pre", "pre-wrap"]);
|
|
55
60
|
// CSS Cascade 4 §7.3 CSS-wide keywords; revert is treated as unset (no user/UA origin split).
|
|
56
61
|
const CSS_WIDE_KEYWORDS = new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
|
|
57
62
|
// Computed-style fields a shorthand (or a property not named after its field) covers.
|
|
58
63
|
const PROPERTY_FIELDS = Object.freeze({
|
|
59
|
-
|
|
64
|
+
overflow: ["overflowX", "overflowY", "overflowScrollX", "overflowScrollY"],
|
|
65
|
+
"overflow-x": ["overflowX", "overflowScrollX"],
|
|
66
|
+
"overflow-y": ["overflowY", "overflowScrollY"],
|
|
67
|
+
background: ["backgroundColor", "backgroundImage", "backgroundPosition", "backgroundSize", "backgroundRepeat"],
|
|
60
68
|
flex: ["flexGrow", "flexShrink", "flexBasis"],
|
|
61
69
|
"flex-flow": ["flexDirection", "flexWrap"],
|
|
62
70
|
gap: ["rowGap", "columnGap", "gap"],
|
|
@@ -89,6 +97,10 @@ const INITIAL = Object.freeze({
|
|
|
89
97
|
colorScheme: "light",
|
|
90
98
|
backgroundColor: "rgba(0, 0, 0, 0)",
|
|
91
99
|
backgroundImage: null,
|
|
100
|
+
// Comma-separated, one entry per background-image layer (repeated as needed); null is the initial value.
|
|
101
|
+
backgroundPosition: null,
|
|
102
|
+
backgroundSize: null,
|
|
103
|
+
backgroundRepeat: null,
|
|
92
104
|
backgroundClip: "border-box",
|
|
93
105
|
boxShadow: null,
|
|
94
106
|
fontFamily: Object.freeze(["sans-serif"]),
|
|
@@ -133,6 +145,8 @@ const INITIAL = Object.freeze({
|
|
|
133
145
|
inset: Object.freeze(["auto", "auto", "auto", "auto"]),
|
|
134
146
|
zIndex: "auto",
|
|
135
147
|
transform: null,
|
|
148
|
+
// Raw text (default "50% 50%"); paint resolves it against the border box.
|
|
149
|
+
transformOrigin: null,
|
|
136
150
|
justifyItems: "normal",
|
|
137
151
|
justifySelf: "auto",
|
|
138
152
|
gridTemplateColumns: null,
|
|
@@ -161,6 +175,10 @@ const INITIAL = Object.freeze({
|
|
|
161
175
|
opacity: 1,
|
|
162
176
|
overflowX: "visible",
|
|
163
177
|
overflowY: "visible",
|
|
178
|
+
// Scroll containers (CSS Overflow 3 §3): "auto" for auto/scroll/overlay,
|
|
179
|
+
// which the user scrolls; "hidden", which only scripts scroll; else null.
|
|
180
|
+
overflowScrollX: null,
|
|
181
|
+
overflowScrollY: null,
|
|
164
182
|
textOverflow: "clip",
|
|
165
183
|
clip: null,
|
|
166
184
|
clipPath: null,
|
|
@@ -187,6 +205,7 @@ const UA_DECLARATIONS = new Map(Object.entries({
|
|
|
187
205
|
FIGURE: "margin: 1em 40px",
|
|
188
206
|
PRE: "margin: 1em 0; font-family: monospace",
|
|
189
207
|
CODE: "font-family: monospace",
|
|
208
|
+
IFRAME: "border: 2px inset",
|
|
190
209
|
KBD: "font-family: monospace",
|
|
191
210
|
SAMP: "font-family: monospace",
|
|
192
211
|
TT: "font-family: monospace",
|
|
@@ -237,6 +256,8 @@ export function controlKind(element) {
|
|
|
237
256
|
}
|
|
238
257
|
|
|
239
258
|
/** HTML §15.3.4: :link (visited links are not tracked). */
|
|
259
|
+
// <iframe frameborder="0"> (or "no"): no border in place of the default one.
|
|
260
|
+
const UA_FRAMELESS_DECLARATIONS = parseDeclarations("border: none");
|
|
240
261
|
const UA_LINK_DECLARATIONS = parseDeclarations("color: #0000EE; text-decoration: underline; cursor: pointer");
|
|
241
262
|
const DECORATION_LINES = new Set(["underline", "overline", "line-through"]);
|
|
242
263
|
const DECORATION_STYLES = new Set(["solid", "double", "dotted", "dashed", "wavy"]);
|
|
@@ -422,6 +443,11 @@ export class StyleEngine {
|
|
|
422
443
|
return this.#styles.get(element) ?? INITIAL;
|
|
423
444
|
}
|
|
424
445
|
|
|
446
|
+
/** An element's custom properties (--*), name to value text, inherited ones included. */
|
|
447
|
+
customPropertiesOf(element) {
|
|
448
|
+
return this.#customProperties.get(element) ?? EMPTY_MAP;
|
|
449
|
+
}
|
|
450
|
+
|
|
425
451
|
/** The computed style of an element's ::before/::after box, or null when it generates none. */
|
|
426
452
|
getPseudo(element, pseudo) {
|
|
427
453
|
return this.#pseudoStyles.get(element)?.[pseudo] ?? null;
|
|
@@ -618,7 +644,9 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
618
644
|
const isLink = (element.tagName === "A" || element.tagName === "AREA") && element.hasAttribute("href");
|
|
619
645
|
const control = controlKind(element);
|
|
620
646
|
if (control === "hidden") values.display = "none";
|
|
621
|
-
|
|
647
|
+
const frameless = element.tagName === "IFRAME" && ["0", "no"].includes(element.getAttribute("frameborder")?.trim().toLowerCase());
|
|
648
|
+
const uaDeclarations = frameless ? UA_FRAMELESS_DECLARATIONS : UA_DECLARATIONS.get(element.tagName);
|
|
649
|
+
for (const declaration of [...(uaDeclarations?.values() ?? []),
|
|
622
650
|
...(isLink ? UA_LINK_DECLARATIONS.values() : []), ...(UA_CONTROL_DECLARATIONS[control]?.values() ?? [])]) {
|
|
623
651
|
if (!declarations.has(declaration.name)) {
|
|
624
652
|
declarations.set(declaration.name, { ...declaration, specificity: -1, order: -1, layer: -Infinity });
|
|
@@ -1112,13 +1140,22 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1112
1140
|
case "overflow":
|
|
1113
1141
|
case "overflow-x":
|
|
1114
1142
|
case "overflow-y": {
|
|
1115
|
-
//
|
|
1143
|
+
// Every non-visible value clips; all but clip also make a scroll container.
|
|
1116
1144
|
const parts = value.split(/\s+/);
|
|
1117
1145
|
const clips = (part) => (["hidden", "clip", "auto", "scroll", "overlay"].includes(part) ? "clip" : part === "visible" ? "visible" : null);
|
|
1118
|
-
const
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1146
|
+
const scrolls = (part) => (["auto", "scroll", "overlay"].includes(part) ? "auto" : part === "hidden" ? "hidden" : null);
|
|
1147
|
+
const xPart = parts[0];
|
|
1148
|
+
const yPart = name === "overflow" ? parts[1] ?? parts[0] : parts[0];
|
|
1149
|
+
const x = clips(xPart);
|
|
1150
|
+
const y = clips(yPart);
|
|
1151
|
+
if (name !== "overflow-y" && x) {
|
|
1152
|
+
style.overflowX = x;
|
|
1153
|
+
style.overflowScrollX = scrolls(xPart);
|
|
1154
|
+
}
|
|
1155
|
+
if (name !== "overflow-x" && y) {
|
|
1156
|
+
style.overflowY = y;
|
|
1157
|
+
style.overflowScrollY = scrolls(yPart);
|
|
1158
|
+
}
|
|
1122
1159
|
break;
|
|
1123
1160
|
}
|
|
1124
1161
|
case "text-overflow":
|
|
@@ -1157,24 +1194,37 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1157
1194
|
break;
|
|
1158
1195
|
}
|
|
1159
1196
|
case "background": {
|
|
1160
|
-
// The shorthand resets
|
|
1161
|
-
const layers = splitTopLevel(value, ",");
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
.
|
|
1165
|
-
.
|
|
1166
|
-
|
|
1167
|
-
style.backgroundImage =
|
|
1197
|
+
// The shorthand resets the longhands; only the final layer may carry a color.
|
|
1198
|
+
const layers = splitTopLevel(value, ",").map(parseBackgroundLayer);
|
|
1199
|
+
if (layers.some((layer) => !layer)) break;
|
|
1200
|
+
style.backgroundColor = layers.at(-1).color
|
|
1201
|
+
? resolvedColor(layers.at(-1).color, style.colorScheme) ?? INITIAL.backgroundColor
|
|
1202
|
+
: INITIAL.backgroundColor;
|
|
1203
|
+
const hasImage = layers.some((layer) => layer.image !== "none");
|
|
1204
|
+
style.backgroundImage = hasImage ? layers.map((layer) => layer.image).join(", ") : null;
|
|
1205
|
+
const list = (field, initial) => layers.some((layer) => layer[field] !== initial)
|
|
1206
|
+
? layers.map((layer) => layer[field]).join(", ")
|
|
1207
|
+
: null;
|
|
1208
|
+
style.backgroundPosition = hasImage ? list("position", "0% 0%") : null;
|
|
1209
|
+
style.backgroundSize = hasImage ? list("size", "auto") : null;
|
|
1210
|
+
style.backgroundRepeat = hasImage ? list("repeat", "repeat") : null;
|
|
1168
1211
|
break;
|
|
1169
1212
|
}
|
|
1170
1213
|
case "background-image": {
|
|
1171
1214
|
if (value === "none") style.backgroundImage = null;
|
|
1172
1215
|
else {
|
|
1173
|
-
const images = splitTopLevel(value, ",").
|
|
1174
|
-
if (images.
|
|
1216
|
+
const images = splitTopLevel(value, ",").map((layer) => BACKGROUND_IMAGE.test(layer) || layer === "none" ? layer : null);
|
|
1217
|
+
if (images.every(Boolean) && images.some((layer) => layer !== "none")) style.backgroundImage = images.join(", ");
|
|
1175
1218
|
}
|
|
1176
1219
|
break;
|
|
1177
1220
|
}
|
|
1221
|
+
case "background-position":
|
|
1222
|
+
case "background-size":
|
|
1223
|
+
case "background-repeat": {
|
|
1224
|
+
const field = { "background-position": "backgroundPosition", "background-size": "backgroundSize", "background-repeat": "backgroundRepeat" }[name];
|
|
1225
|
+
style[field] = value.toLowerCase().replace(/\s+/g, " ");
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1178
1228
|
case "box-shadow": {
|
|
1179
1229
|
const shadows = value === "none" ? null : parseBoxShadows(value, style.fontSize);
|
|
1180
1230
|
if (value === "none" || shadows) style.boxShadow = shadows;
|
|
@@ -1229,10 +1279,13 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1229
1279
|
break;
|
|
1230
1280
|
}
|
|
1231
1281
|
case "transform": {
|
|
1232
|
-
const transform = value === "none" ? null :
|
|
1282
|
+
const transform = value === "none" ? null : parseTransform(value, style.fontSize);
|
|
1233
1283
|
if (transform !== undefined) style.transform = transform;
|
|
1234
1284
|
break;
|
|
1235
1285
|
}
|
|
1286
|
+
case "transform-origin":
|
|
1287
|
+
style.transformOrigin = value.toLowerCase().replace(/\s+/g, " ");
|
|
1288
|
+
break;
|
|
1236
1289
|
case "content": {
|
|
1237
1290
|
const content = parseContent(value);
|
|
1238
1291
|
if (content !== undefined) style.content = content;
|
|
@@ -1296,12 +1349,15 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1296
1349
|
if (value === "normal") {
|
|
1297
1350
|
style.lineHeight = null;
|
|
1298
1351
|
style.lineHeightFactor = null;
|
|
1299
|
-
} else if (Number.isFinite(numeric) && numeric
|
|
1352
|
+
} else if (value !== "" && Number.isFinite(numeric) && numeric >= 0) {
|
|
1300
1353
|
style.lineHeight = numeric * style.fontSize;
|
|
1301
1354
|
style.lineHeightFactor = numeric;
|
|
1302
1355
|
} else {
|
|
1303
|
-
|
|
1304
|
-
|
|
1356
|
+
// Non-negative, so 0 is valid (icon buttons set line-height: 0); a
|
|
1357
|
+
// percentage computes to a length against this element's font size.
|
|
1358
|
+
const percentage = /^(\d+\.?\d*|\.\d+)%$/.exec(value);
|
|
1359
|
+
const length = percentage ? Number(percentage[1]) / 100 * style.fontSize : computedLength(value, style.fontSize);
|
|
1360
|
+
if (length >= 0) {
|
|
1305
1361
|
style.lineHeight = length;
|
|
1306
1362
|
style.lineHeightFactor = null;
|
|
1307
1363
|
}
|
|
@@ -1878,6 +1934,32 @@ function pseudoElement(pseudo) {
|
|
|
1878
1934
|
* percentages of the element's own border box). Other functions are ignored
|
|
1879
1935
|
* until transforms are painted with matrices.
|
|
1880
1936
|
*/
|
|
1937
|
+
/**
|
|
1938
|
+
* The translation (layout moves the box) and scale (paint scales it about
|
|
1939
|
+
* transform-origin) of a transform list; other functions are ignored.
|
|
1940
|
+
*/
|
|
1941
|
+
function parseTransform(value, fontSize) {
|
|
1942
|
+
const translation = parseTranslate(value, fontSize);
|
|
1943
|
+
if (translation === undefined) return undefined;
|
|
1944
|
+
let scaleX = 1;
|
|
1945
|
+
let scaleY = 1;
|
|
1946
|
+
let found = false;
|
|
1947
|
+
for (const match of value.matchAll(/\b(scale|scaleX|scaleY|scale3d)\(([^)]*)\)/gi)) {
|
|
1948
|
+
const args = splitTopLevel(match[2], ",").map((part) => part.endsWith("%") ? Number(part.slice(0, -1)) / 100 : Number(part));
|
|
1949
|
+
if (!args.length || args.some((part) => !Number.isFinite(part))) return undefined;
|
|
1950
|
+
const kind = match[1].toLowerCase();
|
|
1951
|
+
if (kind === "scalex") scaleX *= args[0];
|
|
1952
|
+
else if (kind === "scaley") scaleY *= args[0];
|
|
1953
|
+
else {
|
|
1954
|
+
scaleX *= args[0];
|
|
1955
|
+
scaleY *= args[1] ?? args[0];
|
|
1956
|
+
}
|
|
1957
|
+
found = true;
|
|
1958
|
+
}
|
|
1959
|
+
if (!found || (scaleX === 1 && scaleY === 1)) return translation;
|
|
1960
|
+
return Object.freeze({ x: translation?.x ?? 0, y: translation?.y ?? 0, scaleX, scaleY });
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1881
1963
|
function parseTranslate(value, fontSize) {
|
|
1882
1964
|
let x = 0;
|
|
1883
1965
|
let y = 0;
|
|
@@ -1916,7 +1998,7 @@ function parseContent(value) {
|
|
|
1916
1998
|
* undefined property without a fallback leaves nothing (the declaration then
|
|
1917
1999
|
* has no effect).
|
|
1918
2000
|
*/
|
|
1919
|
-
function resolveVariables(value, customProperties, seen = new Set()) {
|
|
2001
|
+
export function resolveVariables(value, customProperties, seen = new Set()) {
|
|
1920
2002
|
const text = String(value);
|
|
1921
2003
|
// Most values use no custom properties; skip the parsing.
|
|
1922
2004
|
if (!text.includes("var(")) return text.trim();
|
|
@@ -2317,6 +2399,64 @@ function resolvedColor(value, scheme) {
|
|
|
2317
2399
|
return /^rgba\(/.test(color ?? "") ? color : null;
|
|
2318
2400
|
}
|
|
2319
2401
|
|
|
2402
|
+
/**
|
|
2403
|
+
* One layer of the background shorthand, its parts as text: the image, the
|
|
2404
|
+
* position (before a slash) and size (after it), the repeat style and a color.
|
|
2405
|
+
* Null when a token fits none of them.
|
|
2406
|
+
*/
|
|
2407
|
+
function parseBackgroundLayer(layer) {
|
|
2408
|
+
const tokens = [];
|
|
2409
|
+
let depth = 0;
|
|
2410
|
+
let token = "";
|
|
2411
|
+
for (const character of layer) {
|
|
2412
|
+
if (depth === 0 && (/\s/.test(character) || character === "/")) {
|
|
2413
|
+
if (token) tokens.push(token);
|
|
2414
|
+
if (character === "/") tokens.push("/");
|
|
2415
|
+
token = "";
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
if (character === "(") depth++;
|
|
2419
|
+
else if (character === ")") depth--;
|
|
2420
|
+
token += character;
|
|
2421
|
+
}
|
|
2422
|
+
if (token) tokens.push(token);
|
|
2423
|
+
const result = { image: "none", position: "0% 0%", size: "auto", repeat: "repeat", color: null };
|
|
2424
|
+
const position = [];
|
|
2425
|
+
const size = [];
|
|
2426
|
+
const repeat = [];
|
|
2427
|
+
let afterSlash = false;
|
|
2428
|
+
for (const raw of tokens) {
|
|
2429
|
+
const lower = raw.toLowerCase();
|
|
2430
|
+
if (raw === "/") afterSlash = true;
|
|
2431
|
+
else if (BACKGROUND_IMAGE.test(raw)) result.image = raw;
|
|
2432
|
+
else if (lower === "none") result.image = "none";
|
|
2433
|
+
else if (BACKGROUND_REPEATS.has(lower)) repeat.push(lower);
|
|
2434
|
+
else if (BACKGROUND_BOXES.has(lower) || BACKGROUND_ATTACHMENTS.has(lower)) continue;
|
|
2435
|
+
else if (afterSlash && (lower === "cover" || lower === "contain" || lower === "auto" || /^-?[\d.]/.test(lower) || lower.startsWith("calc("))) size.push(lower);
|
|
2436
|
+
else if (["left", "right", "top", "bottom", "center"].includes(lower) || /^-?[\d.]/.test(lower) || lower.startsWith("calc(")) {
|
|
2437
|
+
afterSlash = false;
|
|
2438
|
+
position.push(lower);
|
|
2439
|
+
} else if (resolvedColor(raw, "light")) result.color = raw;
|
|
2440
|
+
else return null;
|
|
2441
|
+
}
|
|
2442
|
+
if (position.length) result.position = position.join(" ");
|
|
2443
|
+
if (size.length) result.size = size.join(" ");
|
|
2444
|
+
if (repeat.length) result.repeat = repeat.join(" ");
|
|
2445
|
+
return result;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
/** A style sheet with each relative url() made absolute against the sheet's own URL. */
|
|
2449
|
+
export function absolutizeStyleSheetUrls(text, base) {
|
|
2450
|
+
return text.replace(/url\(\s*(["']?)([^"')]*)\1\s*\)/gi, (whole, quote, url) => {
|
|
2451
|
+
if (!url || /^(?:[a-z][a-z\d+.-]*:|#)/i.test(url)) return whole;
|
|
2452
|
+
try {
|
|
2453
|
+
return `url(${quote}${new URL(url, base).href}${quote})`;
|
|
2454
|
+
} catch {
|
|
2455
|
+
return whole;
|
|
2456
|
+
}
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2320
2460
|
function splitTopLevel(source, separator) {
|
|
2321
2461
|
const parts = [];
|
|
2322
2462
|
let depth = 0;
|
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.2",
|
|
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
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
diff --git a/packages/happy-dom/src/query-selector/SelectorParser.ts b/packages/happy-dom/src/query-selector/SelectorParser.ts
|
|
2
|
+
index 60cc951..62cf4b9 100644
|
|
3
|
+
--- a/packages/happy-dom/src/query-selector/SelectorParser.ts
|
|
4
|
+
+++ b/packages/happy-dom/src/query-selector/SelectorParser.ts
|
|
5
|
+
@@ -12,10 +12,11 @@ import type { TGlobalMatchFunction } from './TGlobalMatchFunction.js';
|
|
6
|
+
/**
|
|
7
|
+
* Selector group RegExp.
|
|
8
|
+
*
|
|
9
|
+
- * Group 1: Combinator (" ", ",", "+", ">", "̣~")
|
|
10
|
+
- * Group 2: Parentheses or brackets.
|
|
11
|
+
+ * Group 1: An escaped character (e.g. "\:", "\31 "), skipped.
|
|
12
|
+
+ * Group 2: Combinator (" ", ",", "+", ">", "̣~")
|
|
13
|
+
+ * Group 3: Parentheses, brackets or quotes.
|
|
14
|
+
*/
|
|
15
|
+
-const SELECTOR_GROUP_REGEXP = /(\s*[\s,+>~]\s*)|([\[\]\(\)"'])/g;
|
|
16
|
+
+const SELECTOR_GROUP_REGEXP = /(\\[0-9a-fA-F]{1,6}\s?|\\[\s\S])|(\s*[\s,+>~]\s*)|([\[\]\(\)"'])/g;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Selector RegExp.
|
|
20
|
+
@@ -45,7 +46,7 @@ const SELECTOR_GROUP_REGEXP = /(\s*[\s,+>~]\s*)|([\[\]\(\)"'])/g;
|
|
21
|
+
* Group 23: Pseudo element (e.g. "::after", "::-webkit-inner-spin-button").
|
|
22
|
+
*/
|
|
23
|
+
const SELECTOR_REGEXP =
|
|
24
|
+
- /(\*)|([a-zA-Z0-9\u00A0-\uFFFF-]+)|#(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\.)+)|\.(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\.)+)|\[(([a-zA-Z0-9-_]|\\.)+)\]|\[(([a-zA-Z0-9-_]|\\.)+)\s*([~|^$*]{0,1})\s*=\s*("([^"]*)"|'([^']*)')\s*(s|i){0,1}\]|\[(([a-zA-Z0-9-_]|\\.)+)\s*([~|^$*]{0,1})\s*=\s*(([a-zA-Z0-9\u00A0-\uFFFF_¤£-]|\\.)+)\]|:([a-zA-Z-]+)\s*\(.+\)|:([a-zA-Z-]+)|::([a-zA-Z-]+)/g;
|
|
25
|
+
+ /(\*)|((?:[a-zA-Z0-9\u00A0-\uFFFF-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|#(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|\.(([a-zA-Z0-9\u00A0-\uFFFF_-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\]|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\s*([~|^$*]{0,1})\s*=\s*("((?:[^"\\]|\\[\s\S])*)"|'((?:[^'\\]|\\[\s\S])*)')\s*(s|i){0,1}\]|\[(([a-zA-Z0-9-_]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\s*([~|^$*]{0,1})\s*=\s*(([a-zA-Z0-9\u00A0-\uFFFF_¤£-]|\\[0-9a-fA-F]{1,6}\s?|\\.)+)\]|:([a-zA-Z-]+)\s*\(.+\)|:([a-zA-Z-]+)|::([a-zA-Z-]+)/g;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Selector pseudo RegExp.
|
|
29
|
+
@@ -55,11 +56,6 @@ const SELECTOR_REGEXP =
|
|
30
|
+
*/
|
|
31
|
+
const SELECTOR_PSEUDO_REGEXP = /:([a-zA-Z-]+)|([()])/g;
|
|
32
|
+
|
|
33
|
+
-/**
|
|
34
|
+
- * Escaped Character RegExp.
|
|
35
|
+
- */
|
|
36
|
+
-const ESCAPED_CHARACTER_REGEXP = /\\/g;
|
|
37
|
+
-
|
|
38
|
+
/**
|
|
39
|
+
* CSS Escape RegExp.
|
|
40
|
+
*
|
|
41
|
+
@@ -187,6 +183,11 @@ export default class SelectorParser {
|
|
42
|
+
|
|
43
|
+
while ((match = regExp.exec(selector))) {
|
|
44
|
+
if (match[1]) {
|
|
45
|
+
+ // An escaped character (CSS Syntax §4.3.7) is part of the selector it is in,
|
|
46
|
+
+ // never a combinator, bracket or quote.
|
|
47
|
+
+ continue;
|
|
48
|
+
+ }
|
|
49
|
+
+ if (match[2]) {
|
|
50
|
+
// Matches combinator (" ", ",", "+", ">", "̣~")
|
|
51
|
+
// We should ignore combinators that are inside parentheses, brackets or apostrophes
|
|
52
|
+
|
|
53
|
+
@@ -198,7 +199,7 @@ export default class SelectorParser {
|
|
54
|
+
) {
|
|
55
|
+
const childSelector = selector.substring(lastIndex, match.index).trim();
|
|
56
|
+
|
|
57
|
+
- switch (match[1].trim()) {
|
|
58
|
+
+ switch (match[2].trim()) {
|
|
59
|
+
case ',':
|
|
60
|
+
selectorItem = this.getSelectorGroupItem(childSelector, combinator);
|
|
61
|
+
if (!selectorItem) {
|
|
62
|
+
@@ -262,7 +263,7 @@ export default class SelectorParser {
|
|
63
|
+
} else {
|
|
64
|
+
// Matches parentheses or brackets.
|
|
65
|
+
|
|
66
|
+
- switch (match[2]) {
|
|
67
|
+
+ switch (match[3]) {
|
|
68
|
+
case '(':
|
|
69
|
+
if (depth.singleApostrophe === 0 && depth.doubleApostrophe === 0) {
|
|
70
|
+
depth.parentheses++;
|
|
71
|
+
@@ -365,22 +366,22 @@ export default class SelectorParser {
|
|
72
|
+
} else if (match[2]) {
|
|
73
|
+
// Matches tag name, e.g. "div"
|
|
74
|
+
|
|
75
|
+
- selectorItem.tagName = match[2].toUpperCase();
|
|
76
|
+
+ selectorItem.tagName = SelectorParser.cssUnescape(match[2]).toUpperCase();
|
|
77
|
+
} else if (match[3]) {
|
|
78
|
+
// Matches ID, e.g. "#id"
|
|
79
|
+
|
|
80
|
+
- selectorItem.id = match[3].replace(ESCAPED_CHARACTER_REGEXP, '');
|
|
81
|
+
+ selectorItem.id = SelectorParser.cssUnescape(match[3]);
|
|
82
|
+
} else if (match[5]) {
|
|
83
|
+
// Matches class names, e.g. ".class1"
|
|
84
|
+
|
|
85
|
+
selectorItem.classNames = selectorItem.classNames || [];
|
|
86
|
+
- selectorItem.classNames.push(match[5].replace(ESCAPED_CHARACTER_REGEXP, ''));
|
|
87
|
+
+ selectorItem.classNames.push(SelectorParser.cssUnescape(match[5]));
|
|
88
|
+
} else if (match[7]) {
|
|
89
|
+
// Matches attributes without value, e.g. [attr]
|
|
90
|
+
|
|
91
|
+
selectorItem.attributes = selectorItem.attributes || [];
|
|
92
|
+
selectorItem.attributes.push({
|
|
93
|
+
- name: match[7].replace(ESCAPED_CHARACTER_REGEXP, ''),
|
|
94
|
+
+ name: SelectorParser.cssUnescape(match[7]),
|
|
95
|
+
operator: null,
|
|
96
|
+
value: null,
|
|
97
|
+
modifier: null,
|
|
98
|
+
@@ -393,7 +394,7 @@ export default class SelectorParser {
|
|
99
|
+
const unescapedValue = SelectorParser.cssUnescape(value);
|
|
100
|
+
selectorItem.attributes = selectorItem.attributes || [];
|
|
101
|
+
selectorItem.attributes.push({
|
|
102
|
+
- name: match[9].replace(ESCAPED_CHARACTER_REGEXP, ''),
|
|
103
|
+
+ name: SelectorParser.cssUnescape(match[9]),
|
|
104
|
+
operator: match[11] || null,
|
|
105
|
+
value: unescapedValue,
|
|
106
|
+
modifier: <'s'>match[15] || null,
|
|
107
|
+
@@ -409,7 +410,7 @@ export default class SelectorParser {
|
|
108
|
+
const unescapedValue = SelectorParser.cssUnescape(match[19]);
|
|
109
|
+
selectorItem.attributes = selectorItem.attributes || [];
|
|
110
|
+
selectorItem.attributes.push({
|
|
111
|
+
- name: match[16].replace(ESCAPED_CHARACTER_REGEXP, ''),
|
|
112
|
+
+ name: SelectorParser.cssUnescape(match[16]),
|
|
113
|
+
operator: match[18] || null,
|
|
114
|
+
value: unescapedValue,
|
|
115
|
+
modifier: null,
|