Repository: ChristianMurphy/postcss-combine-duplicated-selectors
Pull request #534 (2020-09-10 12:28:39)

feat: add option to only remove exact duplicate property/values
Back in 2017, aldebaran798 added the `removeDuplicatedProperties` feature. I propose to add a "soft" version of this, which I named (at least for now) `removeDuplicatedValues`.

The difference in function is, that it will only remove duplicate properties when the values are equal as well. This way, it cleans up equal properties that came from duplicates of the same selector but allows for conscious duplicates like fallbacks for custom properties, as these have different values and thus won't be removed.


Patch:
diff --git a/README.md b/README.md
index 7208913..f08d10e 100644
--- a/README.md
+++ b/README.md
@@ -110,6 +110,40 @@ will combine into
 }
 ```
 
+In order to limit this to only combining properties when the values are equal, set the `removeDuplicatedValues` option to `true` instead. This could clean up duplicated properties, but allow for conscious duplicates such as fallbacks for custom properties.
+
+```js
+const postcss = require('postcss');
+const combineSelectors = require('postcss-combine-duplicated-selectors');
+
+postcss([combineSelectors({removeDuplicatedValues: true})]);
+```
+
+This will transform the following css
+
+```css
+.a {
+  height: 10px;
+}
+
+.a {
+  width: 20px;
+  background: var(--custom-color);
+  background: rgba(255, 165, 0, 0.5);
+}
+```
+
+into
+
+```css
+.a {
+  height: 10px;
+  width: 20px;
+  background: var(--custom-color);
+  background: rgba(255, 165, 0, 0.5);
+}
+```
+
 ### Media Queries
 
 If you have code with media queries, pass code through [_postcss-combine-media-query_](https://github.com/SassNinja/postcss-combine-media-query) or [_css-mquery-packer_](https://github.com/n19htz/css-mquery-packer) before _postcss-combine-duplicated-selectors_ to ensure optimal results.
diff --git a/src/index.js b/src/index.js
index 66b6881..92068a1 100644
--- a/src/index.js
+++ b/src/index.js
@@ -38,14 +38,21 @@ function sortGroups(selector) {
 /**
  * Remove duplicated properties
  * @param {Object} selector - postcss selector node
+ * @param {Boolean} exact
  */
-function removeDupProperties(selector) {
+function removeDupProperties(selector, exact) {
   // Remove duplicated properties from bottom to top ()
   for (let actIndex = selector.nodes.length - 1; actIndex >= 1; actIndex--) {
     for (let befIndex = actIndex - 1; befIndex >= 0; befIndex--) {
       if (selector.nodes[actIndex].prop === selector.nodes[befIndex].prop) {
-        selector.nodes[befIndex].remove();
-        actIndex--;
+        if (
+          !exact ||
+          (exact &&
+            selector.nodes[actIndex].value === selector.nodes[befIndex].value)
+        ) {
+          selector.nodes[befIndex].remove();
+          actIndex--;
+        }
       }
     }
   }
@@ -79,8 +86,8 @@ module.exports = postcss.plugin(name, (options) => {
 
         // See if this query key is already in the map table
         map = mapTable.has(query) ? // If it is use it
-            mapTable.get(query) : // if not set it and get it
-            mapTable.set(query, new Map()).get(query);
+          mapTable.get(query) : // if not set it and get it
+          mapTable.set(query, new Map()).get(query);
       } else {
         // Otherwise we are dealing with a selector in the root
         map = mapTable.get('root');
@@ -100,12 +107,20 @@ module.exports = postcss.plugin(name, (options) => {
         // remove duplicated rule
         rule.remove();
 
-        if (options.removeDuplicatedProperties) {
-          removeDupProperties(destination);
+        if (options.removeDuplicatedProperties ||
+          options.removeDuplicatedValues) {
+          removeDupProperties(
+              destination,
+              options.removeDuplicatedValues,
+          );
         }
       } else {
-        if (options.removeDuplicatedProperties) {
-          removeDupProperties(rule);
+        if (options.removeDuplicatedProperties ||
+          options.removeDuplicatedValues) {
+          removeDupProperties(
+              rule,
+              options.removeDuplicatedValues,
+          );
         }
         // add new selector to symbol table
         map.set(selector, rule);


Test patch:
diff --git a/test/duplicated-properties.js b/test/duplicated-properties.js
index e8eab0c..197d5b0 100644
--- a/test/duplicated-properties.js
+++ b/test/duplicated-properties.js
@@ -8,7 +8,7 @@ const plugin = require('../src');
  */
 
 /**
- * Take string literals are remove newlines and extra spacing so results print
+ * Take string literals and remove newlines and extra spacing so results print
  * as expected in logs
  * @return {string} string without newlines and tabs
  */
@@ -23,7 +23,7 @@ const removeDuplicates = testFactory(
 );
 
 test(
-    'remove duplicated properties when combine selectors',
+    'remove duplicated properties when combining selectors',
     removeDuplicates,
     '.a {height: 10px; color: black;} .a {color: blue; width: 20px;}',
     '.a {height: 10px;color: blue; width: 20px;}',
@@ -54,7 +54,7 @@ const keepDuplicates = testFactory(
 );
 
 test(
-    'maintain duplicated properties when combine selectors',
+    'maintain duplicated properties when combining selectors',
     keepDuplicates,
     '.a {height: 10px; color: black;} .a {color: blue; width: 20px;}',
     '.a {height: 10px; color: black;color: blue; width: 20px;}',
@@ -78,3 +78,56 @@ test(
 }
 `,
 );
+
+// Only duplicated properties with matching values should be removed
+const removeExactDuplicates = testFactory(
+    'css',
+    [plugin({removeDuplicatedValues: true})],
+);
+
+test(
+    'remove duplicated properties with matching values with combined selectors',
+    removeExactDuplicates,
+    '.a {height: 10px; color: red;} .a {color: red; color: blue; width: 20px;}',
+    '.a {height: 10px;color: red; color: blue; width: 20px;}',
+);
+
+test(
+    'remove duplicated properties with matching values in a selector',
+    removeExactDuplicates,
+    minify`
+.a {
+  height: 10px;
+  background: orange;
+  background: orange;
+  background: rgba(255, 165, 0, 0.5);
+}
+`,
+    minify`
+.a {
+  height: 10px;
+  background: orange;
+  background: rgba(255, 165, 0, 0.5);
+}
+`,
+);
+
+test(
+    'remove duplicate property with matching value, allow fallback',
+    removeExactDuplicates,
+    minify`
+.a {
+  height: 10px;
+}
+.a {
+  height: 10px;
+  height: var(--linkHeight);
+}
+`,
+    minify`
+.a {
+  height: 10px;
+  height: var(--linkHeight);
+}
+`,
+);