Repository: maizzle/framework
Pull request #361 (2020-11-10 16:18:10)

feat(transformers): attribute to style
This PR adds a new `attributeToStyle` Transformer, which allows duplicating certain HTML attributes as inline CSS.

### Supported attributes

#### `width` 

Inlined as: `width: ${value}${unit}`

Notes: supports only `px` and `%` values (defaults to `px`)

#### `height` 

Inlined as: `height: ${value}${unit}`

Notes: supports only `px` and `%` values (defaults to `px`)

#### `bgcolor` 

Inlined as: `background-color: ${value}`

#### `background` 

Inlined as: `background-image: url('${value}')`


#### `align` 

On `<table>` elements: 

- inlines `float: ${value}` for `left` or `right` values
- inlines `margin-left: auto; margin-right: auto` for `center`

On any other elements, gets inlined as `text-align: ${value}`

#### `valign`

Inlined as `vertical-align: ${value}`

### Usage

`attributeToStyle` is disabled by default, so your HTML won't change unless you want it to.

Enable for all supported attributes:

```js
// config.production.js
module.exports = {
  inlineCSS: {
    attributeToStyle: true
  }
}
```

Enable only for these attributes:

```js
// config.production.js
module.exports = {
  inlineCSS: {
    attributeToStyle: ['width', 'bgcolor', 'background']
  }
}
```

### Other Notes

- runs just before the CSS inliner, so that you can still override inlined attributes with Tailwind classes
- doesn't do any CSS parsing, it's intentionally naive and it will _not_ overwrite any existing inline styles
- won't remove the attribute after inlining it - you'll need to use [`removeAttributes`](https://maizzle.com/docs/code-cleanup/#removeattributes) for that


Patch:
diff --git a/src/transformers/attribute-to-style.js b/src/transformers/attribute-to-style.js
new file mode 100644
index 0000000..ba558c8
--- /dev/null
+++ b/src/transformers/attribute-to-style.js
@@ -0,0 +1,82 @@
+const posthtml = require('posthtml')
+const parseAttrs = require('posthtml-attrs-parser')
+const {get, forEach, intersection, keys, isEmpty} = require('lodash')
+
+module.exports = async (html, config) => {
+  const posthtmlOptions = get(config, 'build.posthtml.options', {})
+  const attributes = get(config, 'inlineCSS.attributeToStyle', false)
+
+  if ((typeof attributes === 'boolean' && attributes)) {
+    return posthtml([attributesToStyle()]).process(html, posthtmlOptions).then(result => result.html)
+  }
+
+  if (Array.isArray(attributes) && !isEmpty(attributes)) {
+    return posthtml([attributesToStyle({attributes})]).process(html, posthtmlOptions).then(result => result.html)
+  }
+
+  return html
+}
+
+const attributesToStyle = (options = {}) => tree => {
+  options.attributes = options.attributes || ['width', 'height', 'bgcolor', 'background', 'align', 'valign']
+
+  const process = node => {
+    const nodeAttributes = parseAttrs(node.attrs)
+    const matches = intersection(keys(nodeAttributes), options.attributes)
+    const nodeStyle = get(node.attrs, 'style')
+    const csstoInline = []
+
+    forEach(matches, attribute => {
+      let value = get(node.attrs, attribute)
+
+      switch (attribute) {
+        case 'bgcolor':
+          csstoInline.push(`background-color: ${value}`)
+          break
+
+        case 'background':
+          csstoInline.push(`background-image: url('${value}')`)
+          break
+
+        case 'width':
+          value = Number.parseInt(value, 10) + (value.match(/px|%/) || 'px')
+          csstoInline.push(`width: ${value}`)
+          break
+
+        case 'height':
+          value = Number.parseInt(value, 10) + (value.match(/px|%/) || 'px')
+          csstoInline.push(`height: ${value}`)
+          break
+
+        case 'align':
+          if (node.tag !== 'table') {
+            return csstoInline.push(`text-align: ${value}`)
+          }
+
+          if (['left', 'right'].includes(value)) {
+            csstoInline.push(`float: ${value}`)
+          }
+
+          if (value === 'center') {
+            csstoInline.push('margin-left: auto', 'margin-right: auto')
+          }
+
+          break
+
+        case 'valign':
+          csstoInline.push(`vertical-align: ${value}`)
+          break
+
+        // No default
+      }
+    })
+
+    nodeAttributes.style = nodeStyle ? `${nodeStyle} ${csstoInline.join('; ')}` : `${csstoInline.join('; ')}`
+
+    node.attrs = nodeAttributes.compose()
+
+    return node
+  }
+
+  return tree.walk(process)
+}
diff --git a/src/transformers/index.js b/src/transformers/index.js
index c6a035d..b00b09b 100644
--- a/src/transformers/index.js
+++ b/src/transformers/index.js
@@ -14,12 +14,14 @@ const removeAttributes = require('./remove-attributes')
 const removeInlineSizes = require('./remove-inline-sizes')
 const applyExtraAttributes = require('./extra-attributes')
 const removeInlineBgColor = require('./remove-inline-bgcolor')
+const attributeToStyle = require('./attribute-to-style')
 
 exports.process = async (html, config) => {
   html = await safeClassNames(html, config)
   html = await transformContents(html, config)
   html = await markdown(html, config)
   html = await preventWidows(html, config)
+  html = await attributeToStyle(html, config)
   html = await inline(html, config)
   html = await removeUnusedCSS(html, config)
   html = await removeInlineSizes(html, config)


Test patch:
diff --git a/test/expected/attribute-to-style-tags.html b/test/expected/attribute-to-style-tags.html
new file mode 100644
index 0000000..a665e07
--- /dev/null
+++ b/test/expected/attribute-to-style-tags.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+
+
+  </head>
+<body>
+  <table class="text-xl" width="100%" height="600" bgcolor="#f0f0f0" background="https://example.com/image.jpg" align="left" style="width: 100%; font-size: 1.25rem;" cellpadding="0" cellspacing="0" role="presentation">
+    <tr>
+      <th valign="top">Test TH</th>
+      <th width="200" style="width: 200px">Empty</th>
+    </tr>
+    <tr>
+      <td width="600" height="600" bgcolor="#f0f0f0" background="https://example.com/image.jpg" align="center" valign="middle" style="width: 600px">
+        <img src="https://example.com/image.jpg" width="300" height="200" style="width: 300px" alt="">
+      </td>
+    </tr>
+  </table>
+</body>
+</html>
diff --git a/test/expected/attribute-to-style.html b/test/expected/attribute-to-style.html
new file mode 100644
index 0000000..8261b7a
--- /dev/null
+++ b/test/expected/attribute-to-style.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+
+
+  </head>
+<body>
+  <table class="text-xl" width="100%" height="600" bgcolor="#f0f0f0" background="https://example.com/image.jpg" align="center" style="width: 100%; height: 600px; background-color: #f0f0f0; background-image: url('https://example.com/image.jpg'); margin-left: auto; margin-right: auto; font-size: 1.25rem;" cellpadding="0" cellspacing="0" role="presentation">
+    <tr>
+      <th valign="top" style="vertical-align: top">Test TH</th>
+      <th width="200" style="width: 200px">Empty</th>
+    </tr>
+    <tr>
+      <td width="600" height="600" bgcolor="#f0f0f0" background="https://example.com/image.jpg" align="center" valign="middle" style="width: 600px; height: 600px; background-color: #f0f0f0; background-image: url('https://example.com/image.jpg'); text-align: center; vertical-align: middle">
+        <img src="https://example.com/image.jpg" width="300" height="200" style="width: 300px; height: 200px" alt="">
+      </td>
+    </tr>
+  </table>
+  <table align="left" style="float: left" cellpadding="0" cellspacing="0" role="presentation">
+    <tr>
+      <td>Float left table</td>
+    </tr>
+  </table>
+</body>
+</html>
diff --git a/test/fixtures/attribute-to-style-tags.html b/test/fixtures/attribute-to-style-tags.html
new file mode 100644
index 0000000..58e958e
--- /dev/null
+++ b/test/fixtures/attribute-to-style-tags.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <if condition="page.css">
+    <style>{{{ page.css }}}</style>
+  </if>
+</head>
+<body>
+  <table class="text-xl" width="100%" height="600" bgcolor="#f0f0f0" background="https://example.com/image.jpg" align="left">
+    <tr>
+      <th valign="top">Test TH</th>
+      <th width="200">Empty</th>
+    </tr>
+    <tr>
+      <td width="600" height="600" bgcolor="#f0f0f0" background="https://example.com/image.jpg" align="center" valign="middle">
+        <img src="https://example.com/image.jpg" width="300" height="200">
+      </td>
+    </tr>
+  </table>
+</body>
+</html>
diff --git a/test/fixtures/attribute-to-style.html b/test/fixtures/attribute-to-style.html
new file mode 100644
index 0000000..fecfa52
--- /dev/null
+++ b/test/fixtures/attribute-to-style.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <if condition="page.css">
+    <style>{{{ page.css }}}</style>
+  </if>
+</head>
+<body>
+  <table class="text-xl" width="100%" height="600" bgcolor="#f0f0f0" background="https://example.com/image.jpg" align="center">
+    <tr>
+      <th valign="top">Test TH</th>
+      <th width="200">Empty</th>
+    </tr>
+    <tr>
+      <td width="600" height="600" bgcolor="#f0f0f0" background="https://example.com/image.jpg" align="center" valign="middle">
+        <img src="https://example.com/image.jpg" width="300" height="200">
+      </td>
+    </tr>
+  </table>
+  <table align="left">
+    <tr>
+      <td>Float left table</td>
+    </tr>
+  </table>
+</body>
+</html>
diff --git a/test/test-transformers.js b/test/test-transformers.js
index 2ae2034..311983c 100644
--- a/test/test-transformers.js
+++ b/test/test-transformers.js
@@ -205,3 +205,21 @@ test('url parameters', t => {
     }
   }))
 })
+
+test('attribute to style', t => {
+  return processFile(t, 'attribute-to-style', maizzleConfig({
+    inlineCSS: {
+      enabled: true,
+      attributeToStyle: true
+    }
+  }))
+})
+
+test('attribute to style (with tags)', t => {
+  return processFile(t, 'attribute-to-style-tags', maizzleConfig({
+    inlineCSS: {
+      enabled: true,
+      attributeToStyle: ['width']
+    }
+  }))
+})