diff --git a/package-lock.json b/package-lock.json index 623d9300..e07b1077 100644 --- a/package-lock.json +++ b/package-lock.json @@ -528,6 +528,14 @@ "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.0.tgz", "integrity": "sha512-WpwuBlZ2lQRFa4H/4w49deb9rJLot9KmqrKKjMc9qBl7CID+DdC2swoa34ccRl+anL2B6bLp6TjFdIdnzekMBQ==" }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "requires": { + "is-obj": "1.0.1" + } + }, "ecc-jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", @@ -1036,6 +1044,11 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1120,6 +1133,11 @@ "xtend": "4.0.1" } }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, "is-path-cwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", @@ -3458,6 +3476,16 @@ } } }, + "postcss-selector-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", + "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "requires": { + "dot-prop": "4.2.0", + "indexes-of": "1.0.1", + "uniq": "1.0.1" + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -3894,6 +3922,11 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 4e6f02c8..4d5c6895 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "jsdom": "^11.3.0", "lodash": "^4.13.1", "postcss": "^6.0.14", + "postcss-selector-parser": "3.1.1", "request": "^2.72.0" }, "devDependencies": { diff --git a/src/lib.js b/src/lib.js index 00231538..2b52c4ae 100644 --- a/src/lib.js +++ b/src/lib.js @@ -2,6 +2,7 @@ const jsdom = require('./jsdom.js'), postcss = require('postcss'), + postcssSelectorParser = require('postcss-selector-parser'), _ = require('lodash'); /* Some styles are applied only with user interaction, and therefore its * selectors cannot be used with querySelectorAll. @@ -12,7 +13,7 @@ const dePseudify = (function () { /* link */ ':link', ':visited', /* user action */ - ':hover', ':active', ':focus', + ':hover', ':active', ':focus', ':focus-within', /* UI element states */ ':enabled', ':disabled', ':checked', ':indeterminate', /* form validation */ @@ -28,11 +29,21 @@ const dePseudify = (function () { */ '::?-(?:moz|ms|webkit|o)-[a-z0-9-]+' ], - // Actual regex is of the format: /([^\\])(:hover|:focus)+/ - pseudosRegex = new RegExp('([^\\\\])(' + ignoredPseudos.join('|') + ')+'); + // Actual regex is of the format: /^(:hover|:focus|...)$/i + pseudosRegex = new RegExp('^(' + ignoredPseudos.join('|') + ')$', 'i'); + + function transform (selectors) { + selectors.walkPseudos((selector) => { + if (pseudosRegex.test(selector.value)) { + selector.remove(); + } + }); + } + + const processor = postcssSelectorParser(transform); return function (selector) { - return selector.replace(pseudosRegex, '$1'); + return processor.processSync(selector); }; }()); @@ -233,3 +244,5 @@ module.exports = function uncss(pages, css, ignore) { }]; }); }; + +module.exports.dePseudify = dePseudify; diff --git a/tests/depseudify.js b/tests/depseudify.js new file mode 100644 index 00000000..1a6a90f1 --- /dev/null +++ b/tests/depseudify.js @@ -0,0 +1,41 @@ +'use strict'; + +const expect = require('chai').expect, + { dePseudify } = require('../src/lib'); + +describe('dePseudify() function', () => { + const expected = { + '.clearfix::before': '.clearfix', + '.clearfix:before': '.clearfix', + '.sm\\:hover\\:font-hairline': '.sm\\:hover\\:font-hairline', + '.sm\\:hover\\:font-hairline:hover': '.sm\\:hover\\:font-hairline', + '.sm\\:hover\\:font-hairline\\:hover': '.sm\\:hover\\:font-hairline\\:hover', + '.sm\\:hover\\:font-hairline\\:valid': '.sm\\:hover\\:font-hairline\\:valid', + '.sm\\:valid\\:font-bold:valid': '.sm\\:valid\\:font-bold', + ':focus': '', + ':root a:hover': ':root a', + ':root': ':root', + '[data-text="example of :hover pseudo-class"]:hover': '[data-text="example of :hover pseudo-class"]', + 'a :not(strong):not(span)': 'a :not(strong):not(span)', + 'a:hover :not(strong):not(span)': 'a :not(strong):not(span)', + 'a:nth-child(4n)': 'a:nth-child(4n)', + 'div:FOCUS-WITHIN': 'div', + 'div:focus-within': 'div', + 'h5:hover::before': 'h5', + 'input:checked ~ label': 'input ~ label', + 'input:checked ~ label:before': 'input ~ label', + 'input:nth-child(4n):valid': 'input:nth-child(4n)', + 'li:only-child': 'li:only-child', + 'p:hover:not(.fancy)': 'p:not(.fancy)', + 'p:not(.fancy)': 'p:not(.fancy)', + 'p:not(.fancy):hover': 'p:not(.fancy)' + }; + + Object.keys(expected).forEach((input) => { + const output = expected[input]; + it(`should convert ${input} to ${output || '(empty)'}`, (done) => { + expect(dePseudify(input)).to.equal(output); + done(); + }); + }); +});