Skip to content

has pseudo : fix cleanup of rules in browsers with native support #751

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions plugins/css-has-pseudo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
- Fix: Do not throw when a selector is invalid, show a warning instead.
- Fix: make `:has()` unforgiving. `:has(.foo, :some-invalid-selector)` will no longer match elements that have children with `.foo`.

### 4.0.2 (December 12, 2022)

- Fix: correctly cleanup style rules when a browser has native support. [backported](https://github.com/csstools/postcss-plugins/pull/752)

### 4.0.1 (August 23, 2022)

- Fix: assign global browser polyfill to `window`, `self` or a blank object.
Expand Down
2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/browser-global.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/browser-global.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/browser.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/browser.cjs.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/browser.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/browser.mjs.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/index.cjs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"use strict";var e=require("postcss-selector-parser"),t=require("@csstools/selector-specificity"),s=require("postcss-value-parser");function encodeCSS(e){if(""===e)return"";let t,s="";for(let o=0;o<e.length;o++)t=e.charCodeAt(o).toString(36),s+=0===o?t:"-"+t;return"csstools-has-"+s}function isGuardedByAtSupportsFromAtRuleParams(e){if(!e.toLowerCase().includes(":has("))return!1;let t=!1;try{const o=new Set;s(e).walk((e=>{if("function"===e.type&&"selector"===e.value.toLowerCase())return o.add(s.stringify(e.nodes)),!1})),o.forEach((e=>{selectorContainsHasPseudo(e)&&(t=!0)}))}catch(e){}return t}function selectorContainsHasPseudo(t){if(!t.toLowerCase().includes(":has("))return!1;let s=!1;try{e().astSync(t).walk((e=>{if("pseudo"===e.type&&":has"===e.value.toLowerCase()&&e.nodes&&e.nodes.length>0)return s=!0,!1}))}catch(e){}return s}const creator=s=>{const o={preserve:!0,specificityMatchingName:"does-not-exist",...s||{}},r=":not(#"+o.specificityMatchingName+")",n=":not(."+o.specificityMatchingName+")",a=":not("+o.specificityMatchingName+")";return{postcssPlugin:"css-has-pseudo-experimental",RuleExit:(s,{result:c})=>{if(!s.selector.toLowerCase().includes(":has(")||isWithinSupportCheck(s))return;const i=s.selectors.map((i=>{if(!i.toLowerCase().includes(":has("))return i;let l;try{l=e().astSync(i)}catch(e){return s.warn(c,`Failed to parse selector : "${i}" with message: "${e.message}"`),i}if(void 0===l)return i;l.walkPseudos((t=>{let s=t.parent,r=!1;for(;s;)e.isPseudoClass(s)&&":has"===s.value.toLowerCase()&&(r=!0),s=s.parent;r&&(":visited"===t.value.toLowerCase()&&t.replaceWith(e.className({value:o.specificityMatchingName})),":any-link"===t.value.toLowerCase()&&(t.value=":link"))})),l.walkPseudos((s=>{if(":has"!==s.value.toLowerCase()||!s.nodes)return;let o=s.parent??s;if(o!==s){let t=o.nodes.length;e:for(let s=0;s<o.nodes.length;s++){const r=o.nodes[s];if(e.isPseudoElement(r))for(let e=s-1;e>=0;e--)if("combinator"!==o.nodes[s].type&&"comment"!==o.nodes[s].type){t=e+1;break e}}if(t<o.nodes.length){const s=e.selector({value:"",nodes:[]});o.nodes.slice(0,t).forEach((e=>{delete e.parent,s.append(e)}));const r=e.selector({value:"",nodes:[]});o.nodes.slice(t).forEach((e=>{delete e.parent,r.append(e)}));const n=e.selector({value:"",nodes:[]});n.append(s),n.append(r),o.replaceWith(n),o=s}}const c="["+encodeCSS(o.toString())+"]",i=t.selectorSpecificity(o);let l=c;for(let e=0;e<i.a;e++)l+=r;const u=Math.max(1,i.b)-1;for(let e=0;e<u;e++)l+=n;for(let e=0;e<i.c;e++)l+=a;const p=e().astSync(l);o.replaceWith(p.nodes[0])}));const u=l.toString();return u!==i?".js-has-pseudo "+u:i}));i.join(",")!==s.selectors.join(",")&&(s.cloneBefore({selectors:i}),o.preserve||s.remove())}}};function isWithinSupportCheck(e){let t=e.parent;for(;t;){if("atrule"===t.type&&isGuardedByAtSupportsFromAtRuleParams(t.params))return!0;t=t.parent}return!1}creator.postcss=!0,module.exports=creator;
"use strict";var e=require("postcss-selector-parser"),t=require("@csstools/selector-specificity"),s=require("postcss-value-parser");function encodeCSS(e){if(""===e)return"";let t,s="";for(let o=0;o<e.length;o++)t=e.charCodeAt(o).toString(36),s+=0===o?t:"-"+t;return"csstools-has-"+s}function isGuardedByAtSupportsFromAtRuleParams(e){if(!e.toLowerCase().includes(":has("))return!1;let t=!1;try{const o=new Set;s(e).walk((e=>{if("function"===e.type&&"selector"===e.value.toLowerCase())return o.add(s.stringify(e.nodes)),!1})),o.forEach((e=>{selectorContainsHasPseudo(e)&&(t=!0)}))}catch(e){}return t}function selectorContainsHasPseudo(t){if(!t.toLowerCase().includes(":has("))return!1;let s=!1;try{e().astSync(t).walk((e=>{if("pseudo"===e.type&&":has"===e.value.toLowerCase()&&e.nodes&&e.nodes.length>0)return s=!0,!1}))}catch(e){}return s}const creator=s=>{const o={preserve:!0,specificityMatchingName:"does-not-exist",...s||{}},r=":not(#"+o.specificityMatchingName+")",n=":not(."+o.specificityMatchingName+")",a=":not("+o.specificityMatchingName+")";return{postcssPlugin:"css-has-pseudo",RuleExit:(s,{result:c})=>{if(!s.selector.toLowerCase().includes(":has(")||isWithinSupportCheck(s))return;const i=s.selectors.map((i=>{if(!i.toLowerCase().includes(":has("))return i;let l;try{l=e().astSync(i)}catch(e){return s.warn(c,`Failed to parse selector : "${i}" with message: "${e.message}"`),i}if(void 0===l)return i;l.walkPseudos((t=>{let s=t.parent,r=!1;for(;s;)e.isPseudoClass(s)&&":has"===s.value.toLowerCase()&&(r=!0),s=s.parent;r&&(":visited"===t.value.toLowerCase()&&t.replaceWith(e.className({value:o.specificityMatchingName})),":any-link"===t.value.toLowerCase()&&(t.value=":link"))})),l.walkPseudos((s=>{if(":has"!==s.value.toLowerCase()||!s.nodes)return;let o=s.parent??s;if(o!==s){let t=o.nodes.length;e:for(let s=0;s<o.nodes.length;s++){const r=o.nodes[s];if(e.isPseudoElement(r))for(let e=s-1;e>=0;e--)if("combinator"!==o.nodes[s].type&&"comment"!==o.nodes[s].type){t=e+1;break e}}if(t<o.nodes.length){const s=e.selector({value:"",nodes:[]});o.nodes.slice(0,t).forEach((e=>{delete e.parent,s.append(e)}));const r=e.selector({value:"",nodes:[]});o.nodes.slice(t).forEach((e=>{delete e.parent,r.append(e)}));const n=e.selector({value:"",nodes:[]});n.append(s),n.append(r),o.replaceWith(n),o=s}}const c="["+encodeCSS(o.toString())+"]",i=t.selectorSpecificity(o);let l=c;for(let e=0;e<i.a;e++)l+=r;const u=Math.max(1,i.b)-1;for(let e=0;e<u;e++)l+=n;for(let e=0;e<i.c;e++)l+=a;const p=e().astSync(l);o.replaceWith(p.nodes[0])}));const u=l.toString();return u!==i?".js-has-pseudo "+u:i}));i.join(",")!==s.selectors.join(",")&&(s.cloneBefore({selectors:i}),o.preserve||s.remove())}}};function isWithinSupportCheck(e){let t=e.parent;for(;t;){if("atrule"===t.type&&isGuardedByAtSupportsFromAtRuleParams(t.params))return!0;t=t.parent}return!1}creator.postcss=!0,module.exports=creator;
2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/dist/index.mjs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import e from"postcss-selector-parser";import{selectorSpecificity as t}from"@csstools/selector-specificity";import s from"postcss-value-parser";function encodeCSS(e){if(""===e)return"";let t,s="";for(let o=0;o<e.length;o++)t=e.charCodeAt(o).toString(36),s+=0===o?t:"-"+t;return"csstools-has-"+s}function isGuardedByAtSupportsFromAtRuleParams(e){if(!e.toLowerCase().includes(":has("))return!1;let t=!1;try{const o=new Set;s(e).walk((e=>{if("function"===e.type&&"selector"===e.value.toLowerCase())return o.add(s.stringify(e.nodes)),!1})),o.forEach((e=>{selectorContainsHasPseudo(e)&&(t=!0)}))}catch(e){}return t}function selectorContainsHasPseudo(t){if(!t.toLowerCase().includes(":has("))return!1;let s=!1;try{e().astSync(t).walk((e=>{if("pseudo"===e.type&&":has"===e.value.toLowerCase()&&e.nodes&&e.nodes.length>0)return s=!0,!1}))}catch(e){}return s}const creator=s=>{const o={preserve:!0,specificityMatchingName:"does-not-exist",...s||{}},r=":not(#"+o.specificityMatchingName+")",n=":not(."+o.specificityMatchingName+")",a=":not("+o.specificityMatchingName+")";return{postcssPlugin:"css-has-pseudo-experimental",RuleExit:(s,{result:c})=>{if(!s.selector.toLowerCase().includes(":has(")||isWithinSupportCheck(s))return;const i=s.selectors.map((i=>{if(!i.toLowerCase().includes(":has("))return i;let l;try{l=e().astSync(i)}catch(e){return s.warn(c,`Failed to parse selector : "${i}" with message: "${e.message}"`),i}if(void 0===l)return i;l.walkPseudos((t=>{let s=t.parent,r=!1;for(;s;)e.isPseudoClass(s)&&":has"===s.value.toLowerCase()&&(r=!0),s=s.parent;r&&(":visited"===t.value.toLowerCase()&&t.replaceWith(e.className({value:o.specificityMatchingName})),":any-link"===t.value.toLowerCase()&&(t.value=":link"))})),l.walkPseudos((s=>{if(":has"!==s.value.toLowerCase()||!s.nodes)return;let o=s.parent??s;if(o!==s){let t=o.nodes.length;e:for(let s=0;s<o.nodes.length;s++){const r=o.nodes[s];if(e.isPseudoElement(r))for(let e=s-1;e>=0;e--)if("combinator"!==o.nodes[s].type&&"comment"!==o.nodes[s].type){t=e+1;break e}}if(t<o.nodes.length){const s=e.selector({value:"",nodes:[]});o.nodes.slice(0,t).forEach((e=>{delete e.parent,s.append(e)}));const r=e.selector({value:"",nodes:[]});o.nodes.slice(t).forEach((e=>{delete e.parent,r.append(e)}));const n=e.selector({value:"",nodes:[]});n.append(s),n.append(r),o.replaceWith(n),o=s}}const c="["+encodeCSS(o.toString())+"]",i=t(o);let l=c;for(let e=0;e<i.a;e++)l+=r;const u=Math.max(1,i.b)-1;for(let e=0;e<u;e++)l+=n;for(let e=0;e<i.c;e++)l+=a;const p=e().astSync(l);o.replaceWith(p.nodes[0])}));const u=l.toString();return u!==i?".js-has-pseudo "+u:i}));i.join(",")!==s.selectors.join(",")&&(s.cloneBefore({selectors:i}),o.preserve||s.remove())}}};function isWithinSupportCheck(e){let t=e.parent;for(;t;){if("atrule"===t.type&&isGuardedByAtSupportsFromAtRuleParams(t.params))return!0;t=t.parent}return!1}creator.postcss=!0;export{creator as default};
import e from"postcss-selector-parser";import{selectorSpecificity as t}from"@csstools/selector-specificity";import s from"postcss-value-parser";function encodeCSS(e){if(""===e)return"";let t,s="";for(let o=0;o<e.length;o++)t=e.charCodeAt(o).toString(36),s+=0===o?t:"-"+t;return"csstools-has-"+s}function isGuardedByAtSupportsFromAtRuleParams(e){if(!e.toLowerCase().includes(":has("))return!1;let t=!1;try{const o=new Set;s(e).walk((e=>{if("function"===e.type&&"selector"===e.value.toLowerCase())return o.add(s.stringify(e.nodes)),!1})),o.forEach((e=>{selectorContainsHasPseudo(e)&&(t=!0)}))}catch(e){}return t}function selectorContainsHasPseudo(t){if(!t.toLowerCase().includes(":has("))return!1;let s=!1;try{e().astSync(t).walk((e=>{if("pseudo"===e.type&&":has"===e.value.toLowerCase()&&e.nodes&&e.nodes.length>0)return s=!0,!1}))}catch(e){}return s}const creator=s=>{const o={preserve:!0,specificityMatchingName:"does-not-exist",...s||{}},r=":not(#"+o.specificityMatchingName+")",n=":not(."+o.specificityMatchingName+")",a=":not("+o.specificityMatchingName+")";return{postcssPlugin:"css-has-pseudo",RuleExit:(s,{result:c})=>{if(!s.selector.toLowerCase().includes(":has(")||isWithinSupportCheck(s))return;const i=s.selectors.map((i=>{if(!i.toLowerCase().includes(":has("))return i;let l;try{l=e().astSync(i)}catch(e){return s.warn(c,`Failed to parse selector : "${i}" with message: "${e.message}"`),i}if(void 0===l)return i;l.walkPseudos((t=>{let s=t.parent,r=!1;for(;s;)e.isPseudoClass(s)&&":has"===s.value.toLowerCase()&&(r=!0),s=s.parent;r&&(":visited"===t.value.toLowerCase()&&t.replaceWith(e.className({value:o.specificityMatchingName})),":any-link"===t.value.toLowerCase()&&(t.value=":link"))})),l.walkPseudos((s=>{if(":has"!==s.value.toLowerCase()||!s.nodes)return;let o=s.parent??s;if(o!==s){let t=o.nodes.length;e:for(let s=0;s<o.nodes.length;s++){const r=o.nodes[s];if(e.isPseudoElement(r))for(let e=s-1;e>=0;e--)if("combinator"!==o.nodes[s].type&&"comment"!==o.nodes[s].type){t=e+1;break e}}if(t<o.nodes.length){const s=e.selector({value:"",nodes:[]});o.nodes.slice(0,t).forEach((e=>{delete e.parent,s.append(e)}));const r=e.selector({value:"",nodes:[]});o.nodes.slice(t).forEach((e=>{delete e.parent,r.append(e)}));const n=e.selector({value:"",nodes:[]});n.append(s),n.append(r),o.replaceWith(n),o=s}}const c="["+encodeCSS(o.toString())+"]",i=t(o);let l=c;for(let e=0;e<i.a;e++)l+=r;const u=Math.max(1,i.b)-1;for(let e=0;e<u;e++)l+=n;for(let e=0;e<i.c;e++)l+=a;const p=e().astSync(l);o.replaceWith(p.nodes[0])}));const u=l.toString();return u!==i?".js-has-pseudo "+u:i}));i.join(",")!==s.selectors.join(",")&&(s.cloneBefore({selectors:i}),o.preserve||s.remove())}}};function isWithinSupportCheck(e){let t=e.parent;for(;t;){if("atrule"===t.type&&isGuardedByAtSupportsFromAtRuleParams(t.params))return!0;t=t.parent}return!1}creator.postcss=!0;export{creator as default};
19 changes: 5 additions & 14 deletions plugins/css-has-pseudo/src/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,9 @@ import '@mrhenry/core-web/modules/~element-qsa-has.js';
import extractEncodedSelectors from './encode/extract.mjs';
import encodeCSS from './encode/encode.mjs';

function hasNativeSupport(document) {
function hasNativeSupport() {
try {
// Chrome does not support forgiving selector lists in :has()
document.querySelector(':has(*, :does-not-exist, > *)');
document.querySelector(':has(:has(any))');

// Safari incorrectly returns the html element with this query
if (document.querySelector(':has(:scope *)')) {
return false;
}

Comment on lines -7 to -17
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specification changed so we need to update the feature detection to match.
Functional changes were done in the querySelector(':has()') polyfill from core-web

if (!('CSS' in self) || !('supports' in self.CSS) || !self.CSS.supports(':has(any)')) {
if (!('CSS' in self) || !('supports' in self.CSS) || !self.CSS.supports('selector(:has(div))')) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug fix : needed to check for supports('selector(...)')

return false;
}

Expand All @@ -40,7 +31,7 @@ export default function cssHasPseudo(document, options) {
forcePolyfill: (!!options.forcePolyfill) || false,
};

options.mustPolyfill = options.forcePolyfill || !hasNativeSupport(document);
options.mustPolyfill = options.forcePolyfill || !hasNativeSupport();

if (!Array.isArray(options.observedAttributes)) {
options.observedAttributes = [];
Expand Down Expand Up @@ -238,7 +229,7 @@ export default function cssHasPseudo(document, options) {
function walkStyleSheet(styleSheet) {
try {
// walk a css rule to collect observed css rules
[].forEach.call(styleSheet.cssRules || [], (rule) => {
[].forEach.call(styleSheet.cssRules || [], (rule, index) => {
if (rule.selectorText) {
rule.selectorText = rule.selectorText.replace(/\.js-has-pseudo\s/g, '');

Expand All @@ -250,7 +241,7 @@ export default function cssHasPseudo(document, options) {
}

if (!options.mustPolyfill) {
rule.deleteRule();
styleSheet.deleteRule(index);
Copy link
Member Author

@romainmenke romainmenke Dec 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the actual bug.
The fallback rules were never removed but the polyfill wasn't active because Chrome has native support.

Combined with :not() you can get funky results.

With this change the polyfill correctly cleans up the fallback rules for browsers with native support.

return;
}

Expand Down
2 changes: 1 addition & 1 deletion plugins/css-has-pseudo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
const specificityMatchingNameTag = ':not(' + options.specificityMatchingName + ')';

return {
postcssPlugin: 'css-has-pseudo-experimental',
postcssPlugin: 'css-has-pseudo',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left over from experimental track.

RuleExit: (rule, { result }) => {
if (!rule.selector.toLowerCase().includes(':has(') || isWithinSupportCheck(rule)) {
return;
Expand Down