Skip to content

Commit

Permalink
make :has() unforgiving (#1374)
Browse files Browse the repository at this point in the history
  • Loading branch information
romainmenke committed Dec 9, 2022
1 parent dda1e6f commit 15244e0
Show file tree
Hide file tree
Showing 14 changed files with 308 additions and 232 deletions.
275 changes: 162 additions & 113 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -10,7 +10,7 @@
"prettier": {
"useTabs": true
},
"version": "1.0.2",
"version": "1.0.3",
"volta": {
"node": "18.10.0"
}
Expand Down
4 changes: 2 additions & 2 deletions packages/babel-plugin-core-web/package.json
@@ -1,6 +1,6 @@
{
"name": "@mrhenry/babel-plugin-core-web",
"version": "1.0.2",
"version": "1.0.3",
"description": "browser feature polyfills as a babel plugin",
"main": "lib/index.js",
"author": "Simon Menke <simon.menke@gmail.com>",
Expand Down Expand Up @@ -33,7 +33,7 @@
"dependencies": {
"@babel/helper-module-imports": "^7.15.4",
"@babel/types": "^7.18.13",
"@mrhenry/core-web": "^1.0.2",
"@mrhenry/core-web": "^1.0.3",
"fast-deep-equal": "^3.1.3"
},
"bugs": {
Expand Down
2 changes: 1 addition & 1 deletion packages/core-web-example/package.json
@@ -1,6 +1,6 @@
{
"name": "@mrhenry/core-web-example",
"version": "1.0.2",
"version": "1.0.3",
"private": true,
"scripts": {
"build": "babel src -d lib"
Expand Down
2 changes: 1 addition & 1 deletion packages/core-web-generator/package.json
@@ -1,6 +1,6 @@
{
"name": "@mrhenry/core-web-generator",
"version": "1.0.2",
"version": "1.0.3",
"private": true,
"scripts": {
"clean": "rm -rf ./lib/",
Expand Down
109 changes: 59 additions & 50 deletions packages/core-web-generator/polyfills/~element-qsa-has.js
Expand Up @@ -4,11 +4,6 @@
// test for has support
global.document.querySelector(':has(*, :does-not-exist, > *)');

// see : https://github.com/w3c/csswg-drafts/issues/7676
// Fully invalid lists are not forgiving because of jQuery :has()
// We do not match the shipped behavior of Chrome and Safari because it conflicts with the specification.
global.document.querySelector(':has(:has(any), div)');

if (
!global.document.querySelector(':has(:scope *)') &&
CSS.supports('selector(:has(div))')
Expand Down Expand Up @@ -407,10 +402,10 @@
var innerPart = innerParts[i];

// Nested has is not supported.
// If a recursive/nested call returns "false" we replace with ":not(*)"
// If a recursive/nested call returns "false" we throw
var innerPartReplaced = replaceAllWithTempAttr(innerPart, true, function () { });
if (!innerPartReplaced) {
newInnerParts.push(':not(*)');
throw new Error("Nested :has() is not supported")
} else {
newInnerParts.push(innerPart);
}
Expand Down Expand Up @@ -498,57 +493,53 @@
absoluteSelectorPart = ':scope ' + selectorPart;
}

try {
walkNode(rootNode, function (node) {
if (!(node.querySelector(absoluteSelectorPart))) {
return;
}

switch (selectorPart[0]) {
case '~':
case '+':
{
var siblings = node.childNodes;
for (var i = 0; i < siblings.length; i++) {
var sibling = siblings[i];
if (!('setAttribute' in sibling)) {
continue;
}

var idAttr = 'q-has-id' + (Math.floor(Math.random() * 9000000) + 1000000);
sibling.setAttribute(idAttr, '');

if (node.querySelector(':scope [' + idAttr + ']' + ' ' + selectorPart)) {
sibling.setAttribute(attr, '');
}

sibling.removeAttribute(idAttr);
walkNode(rootNode, function (node) {
if (!(node.querySelector(absoluteSelectorPart))) {
return;
}

switch (selectorPart[0]) {
case '~':
case '+':
{
var siblings = node.childNodes;
for (var i = 0; i < siblings.length; i++) {
var sibling = siblings[i];
if (!('setAttribute' in sibling)) {
continue;
}
}
break;

case '>':
{
var idAttr = 'q-has-id' + (Math.floor(Math.random() * 9000000) + 1000000);
node.setAttribute(idAttr, '');
sibling.setAttribute(idAttr, '');

if (node.querySelector(':scope[' + idAttr + ']' + ' ' + selectorPart)) {
node.setAttribute(attr, '');
if (node.querySelector(':scope [' + idAttr + ']' + ' ' + selectorPart)) {
sibling.setAttribute(attr, '');
}

node.removeAttribute(idAttr);
sibling.removeAttribute(idAttr);
}
break;
}
break;

default:
node.setAttribute(attr, '');
case '>':
{
var idAttr = 'q-has-id' + (Math.floor(Math.random() * 9000000) + 1000000);
node.setAttribute(idAttr, '');

break;
}
});
} catch (_) {
// `:has` takes a forgiving selector list.
}
if (node.querySelector(':scope[' + idAttr + ']' + ' ' + selectorPart)) {
node.setAttribute(attr, '');
}

node.removeAttribute(idAttr);
}
break;

default:
node.setAttribute(attr, '');

break;
}
});
}
});

Expand Down Expand Up @@ -596,7 +587,25 @@
}
}

throw err;
var errorMessage = '';
try {
qsa.apply(this, [':core-web-does-not-exist']);
} catch (dummyError) {
errorMessage = dummyError.message;
if (errorMessage) {
errorMessage = errorMessage.replace(':core-web-does-not-exist', selectors);
}
}

if (!errorMessage) {
errorMessage = "Failed to execute 'querySelector' on 'Document': '" + selectors + "' is not a valid selector.";
}

try {
throw new DOMException(errorMessage);
} catch (_) {
throw new Error(errorMessage);
}
}
};
}
Expand Down
6 changes: 3 additions & 3 deletions packages/core-web-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@mrhenry/core-web-tests",
"version": "1.0.2",
"version": "1.0.3",
"private": true,
"scripts": {
"build": "webpack",
Expand All @@ -11,8 +11,8 @@
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/preset-env": "^7.16.11",
"@mrhenry/babel-plugin-core-web": "^1.0.2",
"@mrhenry/core-web": "^1.0.2",
"@mrhenry/babel-plugin-core-web": "^1.0.3",
"@mrhenry/core-web": "^1.0.3",
"babel-loader": "^9.0.0",
"browserstack-runner": "^0.9.4",
"core-js": "^3.21.0",
Expand Down
21 changes: 15 additions & 6 deletions packages/core-web-tests/src/test_querySelectorAll_has_pseudo.js
Expand Up @@ -55,8 +55,11 @@ if ("Proxy" in self) {
assert.ok(document.body.querySelector(":has(*)"));
});

QUnit.test("accepts broken selector lists", function (assert) {
assert.ok(document.body.querySelector(":has(*, :does-not-exist)"));
// TODO : re-enable after browsers ship unforgiving `:has()`
QUnit.skip("does not accept broken selector lists", function (assert) {
assert.throws(() => {
document.body.querySelector(":has(*, :does-not-exist)");
});
});

// Edge does not support this test. Manually enable this when needed.
Expand Down Expand Up @@ -875,12 +878,18 @@ if ("Proxy" in self) {
]);
});

QUnit.test(":has nested as forgiven list", function (assert) {
assert.ok(!document.querySelector(":has(.does-not-exist, :has(#qunit-fixture))"), "nested :has is ignored in forgiving selector lists");
// TODO : re-enable after browsers ship unforgiving `:has()`
QUnit.skip(":has nested as unforgiven list", function (assert) {
assert.throws(() => {
document.querySelector(":has(.does-not-exist, :has(#qunit-fixture))");
}, "nested :has throws in unforgiving selector lists");
});

QUnit.skip(":has nested as forgiven list with a fully invalid list", function (assert) {
assert.ok(!document.querySelector(":has(:has(#qunit-fixture))"), "nested :has is ignored in forgiving selector lists");
// TODO : re-enable after browsers ship unforgiving `:has()`
QUnit.skip(":has nested as unforgiven list with a fully invalid list", function (assert) {
assert.throws(() => {
document.querySelector(":has(:has(#qunit-fixture))");
}, "nested :has throws in unforgiving selector lists");
});
});
}
2 changes: 1 addition & 1 deletion packages/core-web/__mapping.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/core-web/lib/__mapping.js

Large diffs are not rendered by default.

109 changes: 59 additions & 50 deletions packages/core-web/modules/~element-qsa-has.js
Expand Up @@ -4,11 +4,6 @@
// test for has support
global.document.querySelector(':has(*, :does-not-exist, > *)');

// see : https://github.com/w3c/csswg-drafts/issues/7676
// Fully invalid lists are not forgiving because of jQuery :has()
// We do not match the shipped behavior of Chrome and Safari because it conflicts with the specification.
global.document.querySelector(':has(:has(any), div)');

if (
!global.document.querySelector(':has(:scope *)') &&
CSS.supports('selector(:has(div))')
Expand Down Expand Up @@ -407,10 +402,10 @@
var innerPart = innerParts[i];

// Nested has is not supported.
// If a recursive/nested call returns "false" we replace with ":not(*)"
// If a recursive/nested call returns "false" we throw
var innerPartReplaced = replaceAllWithTempAttr(innerPart, true, function () { });
if (!innerPartReplaced) {
newInnerParts.push(':not(*)');
throw new Error("Nested :has() is not supported")
} else {
newInnerParts.push(innerPart);
}
Expand Down Expand Up @@ -498,57 +493,53 @@
absoluteSelectorPart = ':scope ' + selectorPart;
}

try {
walkNode(rootNode, function (node) {
if (!(node.querySelector(absoluteSelectorPart))) {
return;
}

switch (selectorPart[0]) {
case '~':
case '+':
{
var siblings = node.childNodes;
for (var i = 0; i < siblings.length; i++) {
var sibling = siblings[i];
if (!('setAttribute' in sibling)) {
continue;
}

var idAttr = 'q-has-id' + (Math.floor(Math.random() * 9000000) + 1000000);
sibling.setAttribute(idAttr, '');

if (node.querySelector(':scope [' + idAttr + ']' + ' ' + selectorPart)) {
sibling.setAttribute(attr, '');
}

sibling.removeAttribute(idAttr);
walkNode(rootNode, function (node) {
if (!(node.querySelector(absoluteSelectorPart))) {
return;
}

switch (selectorPart[0]) {
case '~':
case '+':
{
var siblings = node.childNodes;
for (var i = 0; i < siblings.length; i++) {
var sibling = siblings[i];
if (!('setAttribute' in sibling)) {
continue;
}
}
break;

case '>':
{
var idAttr = 'q-has-id' + (Math.floor(Math.random() * 9000000) + 1000000);
node.setAttribute(idAttr, '');
sibling.setAttribute(idAttr, '');

if (node.querySelector(':scope[' + idAttr + ']' + ' ' + selectorPart)) {
node.setAttribute(attr, '');
if (node.querySelector(':scope [' + idAttr + ']' + ' ' + selectorPart)) {
sibling.setAttribute(attr, '');
}

node.removeAttribute(idAttr);
sibling.removeAttribute(idAttr);
}
break;
}
break;

default:
node.setAttribute(attr, '');
case '>':
{
var idAttr = 'q-has-id' + (Math.floor(Math.random() * 9000000) + 1000000);
node.setAttribute(idAttr, '');

break;
}
});
} catch (_) {
// `:has` takes a forgiving selector list.
}
if (node.querySelector(':scope[' + idAttr + ']' + ' ' + selectorPart)) {
node.setAttribute(attr, '');
}

node.removeAttribute(idAttr);
}
break;

default:
node.setAttribute(attr, '');

break;
}
});
}
});

Expand Down Expand Up @@ -596,7 +587,25 @@
}
}

throw err;
var errorMessage = '';
try {
qsa.apply(this, [':core-web-does-not-exist']);
} catch (dummyError) {
errorMessage = dummyError.message;
if (errorMessage) {
errorMessage = errorMessage.replace(':core-web-does-not-exist', selectors);
}
}

if (!errorMessage) {
errorMessage = "Failed to execute 'querySelector' on 'Document': '" + selectors + "' is not a valid selector.";
}

try {
throw new DOMException(errorMessage);
} catch (_) {
throw new Error(errorMessage);
}
}
};
}
Expand Down

0 comments on commit 15244e0

Please sign in to comment.