Repository: maizzle/framework
Pull request #786 (2022-10-09 00:11:53)

refactor: parsing attributes in transformers
This PR refactors attribute parsing in some transformers to solve an issue where attributes with no value were being quoted even when PostHTML was configured with the `recognizeNoValueAttribute` option.

Basically, if you had this in your `config.js`:

```js
module.exports = {
  build: {
    posthtml: {
      options: {
        recognizeNoValueAttribute: true
      }
    }
  }
}
```

... Maizzle was compiling this:

```html
<a href="report.pdf" download>
  Download report
</a>
```

... to this:

```html
<a href="report.pdf" download="">
  Download report
</a>
```

Notice the extra `=""`. 

This PR ensures that attributes are output as they were written (as long as you have set the `recognizeNoValueAttribute` option).


Patch:
diff --git a/src/transformers/removeInlineBackgroundColor.js b/src/transformers/removeInlineBackgroundColor.js
index df38d48..a649657 100644
--- a/src/transformers/removeInlineBackgroundColor.js
+++ b/src/transformers/removeInlineBackgroundColor.js
@@ -1,6 +1,7 @@
 const posthtml = require('posthtml')
 const {get, isEmpty} = require('lodash')
 const parseAttrs = require('posthtml-attrs-parser')
+const {toStyleString} = require('../utils/helpers')
 
 module.exports = async (html, config = {}, direct = false) => {
   const posthtmlOptions = get(config, 'build.posthtml.options', {})
@@ -40,9 +41,11 @@ const removeInlineBGColor = (options = {}) => tree => {
     })
 
     if (attrs.style && attrs.style['background-color']) {
-      attrs.bgcolor = attrs.style['background-color']
+      node.attrs.bgcolor = attrs.style['background-color']
+
       delete attrs.style['background-color']
-      node.attrs = attrs.compose()
+
+      node.attrs.style = toStyleString(attrs.style)
     }
 
     return node
diff --git a/src/transformers/removeInlineSizes.js b/src/transformers/removeInlineSizes.js
index c2549ae..98392d7 100644
--- a/src/transformers/removeInlineSizes.js
+++ b/src/transformers/removeInlineSizes.js
@@ -1,6 +1,7 @@
 const posthtml = require('posthtml')
 const {get, isEmpty} = require('lodash')
 const parseAttrs = require('posthtml-attrs-parser')
+const {toStyleString} = require('../utils/helpers')
 
 module.exports = async (html, config = {}, direct = false) => {
   const settings = direct ? config : get(config, 'inlineCSS.keepOnlyAttributeSizes', {})
@@ -26,14 +27,14 @@ const removeInlineSizes = (mappings = {}) => tree => {
       }
 
       tags.forEach(() => {
-        if (attrs.style) {
+        if (get(node, 'attrs.style')) {
           delete attrs.style[attribute]
+
+          node.attrs.style = toStyleString(attrs.style)
         }
       })
     })
 
-    node.attrs = attrs.compose()
-
     return node
   }
 
diff --git a/src/transformers/removeInlinedSelectors.js b/src/transformers/removeInlinedSelectors.js
index faeb5e8..ec484ae 100644
--- a/src/transformers/removeInlinedSelectors.js
+++ b/src/transformers/removeInlinedSelectors.js
@@ -10,10 +10,10 @@ module.exports = async (html, config = {}) => {
   }
 
   const posthtmlOptions = get(config, 'build.posthtml.options', {})
-  return posthtml([plugin()]).process(html, posthtmlOptions).then(result => result.html)
+  return posthtml([plugin(posthtmlOptions)]).process(html, posthtmlOptions).then(result => result.html)
 }
 
-const plugin = () => tree => {
+const plugin = posthtmlOptions => tree => {
   const process = node => {
     // For each style tag...
     if (node.tag === 'style') {
@@ -57,6 +57,13 @@ const plugin = () => tree => {
 
             n.attrs = parsedAttrs.compose()
 
+            // Fix issue with .compose() automatically quoting attributes with no values
+            Object.entries(n.attrs).forEach(([name, value]) => {
+              if (value === '' && get(posthtmlOptions, 'recognizeNoValueAttribute') === true) {
+                n.attrs[name] = true
+              }
+            })
+
             return n
           })
         } catch {}
diff --git a/src/transformers/sixHex.js b/src/transformers/sixHex.js
index 74d638d..966fb03 100644
--- a/src/transformers/sixHex.js
+++ b/src/transformers/sixHex.js
@@ -1,6 +1,5 @@
 const {get} = require('lodash')
 const posthtml = require('posthtml')
-const parseAttrs = require('posthtml-attrs-parser')
 const {conv} = require('color-shorthand-hex-to-six-digit')
 
 module.exports = async (html, config = {}) => {
@@ -13,18 +12,16 @@ module.exports = async (html, config = {}) => {
 }
 
 const sixHex = () => tree => {
-  const process = node => {
-    const attrs = parseAttrs(node.attrs)
-
-    const targets = ['bgcolor', 'color']
+  const targets = new Set(['bgcolor', 'color'])
 
-    targets.forEach(attribute => {
-      if (attrs[attribute]) {
-        attrs[attribute] = conv(attrs[attribute])
-      }
-    })
-
-    node.attrs = attrs.compose()
+  const process = node => {
+    if (node.attrs) {
+      Object.entries(node.attrs).forEach(([name, value]) => {
+        if (targets.has(name) && node.attrs[name]) {
+          node.attrs[name] = conv(value)
+        }
+      })
+    }
 
     return node
   }
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index c9fb216..bc6d26d 100644
--- a/src/utils/helpers.js
+++ b/src/utils/helpers.js
@@ -13,5 +13,6 @@ module.exports = {
     }
   },
   // https://github.com/lukeed/console-clear
-  clearConsole: () => process.stdout.write('\x1B[H\x1B[2J')
+  clearConsole: () => process.stdout.write('\x1B[H\x1B[2J'),
+  toStyleString: (object = {}) => Object.entries(object).map(([k, v]) => `${k}: ${v}`).join('; ')
 }


Test patch:
diff --git a/test/test-transformers.js b/test/test-transformers.js
index f8fbe1c..be0828f 100644
--- a/test/test-transformers.js
+++ b/test/test-transformers.js
@@ -427,7 +427,7 @@ test('remove inlined selectors', async t => {
       </style>
     </head>
     <body>
-      <div id="keepId" class="remove keep ignore foo-class" style="color: red; display: inline">
+      <div no-value id="keepId" class="remove keep ignore foo-class" style="color: red; display: inline">
         <h1 class="m-0 mb-4 mt-0 hover-text-blue" style="margin: 0 0 16px;">Title</h1>
         <img src="https://example.com/image.jpg" style="border: 0; vertical-align: middle">
         <div id="keepId" class="remove keep ignore" style="color: red; display: inline">text</div>
@@ -435,7 +435,7 @@ test('remove inlined selectors', async t => {
     </body>
   </html>`
 
-  const expected = `<!DOCTYPE html>
+  const expectedWithOptions = `<!DOCTYPE html>
   <html>
     <head>
       <style>
@@ -461,7 +461,7 @@ test('remove inlined selectors', async t => {
       </style>
     </head>
     <body>
-      <div id="keepId" class="keep foo-class" style="color: red; display: inline">
+      <div no-value id="keepId" class="keep foo-class" style="color: red; display: inline">
         <h1 class="hover-text-blue" style="margin: 0 0 16px">Title</h1>
         <img src="https://example.com/image.jpg" style="border: 0; vertical-align: middle">
         <div id="keepId" class="keep" style="color: red; display: inline">text</div>
@@ -469,9 +469,22 @@ test('remove inlined selectors', async t => {
     </body>
   </html>`
 
-  const result = await Maizzle.removeInlinedClasses(html)
+  const withPostHTMLOptions = await Maizzle.removeInlinedClasses(html, {
+    build: {
+      posthtml: {
+        options: {
+          recognizeNoValueAttribute: true
+        }
+      }
+    }
+  })
+
+  const basic = await Maizzle.removeInlinedClasses(html)
 
-  t.is(result, expected)
+  const expectedBasic = expectedWithOptions.replace('no-value', 'no-value=""')
+
+  t.is(withPostHTMLOptions, expectedWithOptions)
+  t.is(basic, expectedBasic)
 })
 
 test('remove inlined selectors (disabled)', async t => {