Repository: Automattic/juice
Pull request #314 (2018-09-25 12:42:10)

Preserve pseudos
This adds a new  `preservePseudos` option. 

It preserves all rules containing pseudo selectors defined in `ignoredPseudos` within `<style></style>` tags as a refinement when `removeStyleTags` is `true`. Other styles are removed. Defaults to `true`.

---

Note: I've decided to go for a boolean option instead of something like an array, since there already is `ignoredPseudos` we can defined them in. Please let me know if this is ok. Thanks!


Patch:
diff --git a/README.md b/README.md
index d4a6904..8151394 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,8 @@ All juice methods take an options object that can contain any of these propertie
 
 * `preserveKeyFrames` - preserves all key frames within `<style></style>` tags as a refinement when `removeStyleTags` is `true`. Other styles are removed. Defaults to `true`.
 
+* `preservePseudos` - preserves all rules containing pseudo selectors defined in `ignoredPseudos` within `<style></style>` tags as a refinement when `removeStyleTags` is `true`. Other styles are removed. Defaults to `true`.
+
 * `removeStyleTags` - whether to remove the original `<style></style>` tags after (possibly) inlining the css from them. Defaults to `true`.
 
 * `webResources` - An options object that will be passed to [web-resource-inliner](https://www.npmjs.com/package/web-resource-inliner) for juice functions that will get remote resources (`juiceResources` and `juiceFile`). Defaults to `{}`.
diff --git a/juice.d.ts b/juice.d.ts
index 854277b..e90541d 100644
--- a/juice.d.ts
+++ b/juice.d.ts
@@ -40,6 +40,7 @@ declare namespace juice {
     preserveMediaQueries?: boolean;
     preserveFontFaces?: boolean;
     preserveKeyFrames?: boolean;
+    preservePseudos?: boolean;
     insertPreservedExtraCss?: boolean;
     applyWidthAttributes?: boolean;
     applyHeightAttributes?: boolean;
diff --git a/lib/cli.js b/lib/cli.js
index 2255525..384b5dc 100644
--- a/lib/cli.js
+++ b/lib/cli.js
@@ -61,6 +61,10 @@ cli.options = {
     pMap: 'preserveKeyFrames',
     def: 'preserve key frames?',
     coercion: JSON.parse },
+  'preserve-pseudos': {
+    pMap: 'preservePseudos',
+    def: 'preserve pseudo selectors?',
+    coercion: JSON.parse },
   'apply-width-attributes': {
     pMap: 'applyWidthAttributes',
     def: 'apply width attributes to relevent elements?',
diff --git a/lib/inline.js b/lib/inline.js
index 60345e5..5995526 100644
--- a/lib/inline.js
+++ b/lib/inline.js
@@ -348,7 +348,8 @@ function getStylesData($, options) {
       var preservedText = utils.getPreservedText(styleElement.childNodes[0].nodeValue, {
         mediaQueries: options.preserveMediaQueries,
         fontFaces: options.preserveFontFaces,
-        keyFrames: options.preserveKeyFrames
+        keyFrames: options.preserveKeyFrames,
+        pseudos: juiceClient.ignoredPseudos
       });
       if (preservedText) {
         styleElement.childNodes[0].nodeValue = preservedText;
diff --git a/lib/utils.js b/lib/utils.js
index ba4e211..632e10d 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -94,7 +94,8 @@ exports.getPreservedText = function(css, options) {
   for (var i = rules.length - 1; i >= 0; i--) {
     if ((options.fontFaces && rules[i].type === 'font-face') ||
         (options.mediaQueries && rules[i].type === 'media') ||
-        (options.keyFrames && rules[i].type === 'keyframes')) {
+        (options.keyFrames && rules[i].type === 'keyframes') ||
+        (options.pseudos && rules[i].selectors && this.matchesPseudo(rules[i].selectors[0], options.pseudos))) {
       preserved.unshift(
         mensch.stringify(
           { stylesheet: { rules: [ rules[i] ] }},
@@ -115,6 +116,11 @@ exports.normalizeLineEndings = function(text) {
   return text.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
 };
 
+exports.matchesPseudo = function(needle, haystack) {
+  return haystack.find(function (element) {
+    return needle.indexOf(element) > -1;
+  })
+}
 
 /**
  * Compares two specificity vectors, returning the winning one.
@@ -158,6 +164,7 @@ exports.getDefaultOptions = function(options) {
     preserveMediaQueries: true,
     preserveFontFaces: true,
     preserveKeyFrames: true,
+    preservePseudos: true,
     applyWidthAttributes: true,
     applyHeightAttributes: true,
     applyAttributesTableElements: true,


Test patch:
diff --git a/test/cases/juice-content/pseudo-selector-preserve.css b/test/cases/juice-content/pseudo-selector-preserve.css
new file mode 100644
index 0000000..e69de29
diff --git a/test/cases/juice-content/pseudo-selector-preserve.html b/test/cases/juice-content/pseudo-selector-preserve.html
new file mode 100644
index 0000000..50e0751
--- /dev/null
+++ b/test/cases/juice-content/pseudo-selector-preserve.html
@@ -0,0 +1,24 @@
+<html>
+<head>
+    <style>
+        a:hover {
+            color: red;
+        }
+        a:active {
+            color: red;
+        }
+        a:focus {
+            color: red;
+        }
+        a:visited {
+            color: red;
+        }
+        a:link {
+            color: red;
+        }
+    </style>
+</head>
+<body>
+    <a href="#">hover me</a>
+</body>
+</html>
diff --git a/test/cases/juice-content/pseudo-selector-preserve.json b/test/cases/juice-content/pseudo-selector-preserve.json
new file mode 100644
index 0000000..4ba1fae
--- /dev/null
+++ b/test/cases/juice-content/pseudo-selector-preserve.json
@@ -0,0 +1,5 @@
+{
+    "url": "./",
+    "removeStyleTags": true,
+    "preservePseudos": true
+}
diff --git a/test/cases/juice-content/pseudo-selector-preserve.out b/test/cases/juice-content/pseudo-selector-preserve.out
new file mode 100644
index 0000000..e502764
--- /dev/null
+++ b/test/cases/juice-content/pseudo-selector-preserve.out
@@ -0,0 +1,24 @@
+<html>
+<head>
+    <style>
+a:hover {
+  color: red;
+}
+a:active {
+  color: red;
+}
+a:focus {
+  color: red;
+}
+a:visited {
+  color: red;
+}
+a:link {
+  color: red;
+}
+</style>
+</head>
+<body>
+    <a href="#">hover me</a>
+</body>
+</html>
\ No newline at end of file