Repository: maizzle/framework
Pull request #801 (2022-10-13 13:45:23)

refactor: add backend delimiters defaults to removeUnusedCSS
This PR adds a default safelist for the `{{ }}` and `{% %}` delimiters when using the `removeUnusedCSS` transformer, so that you don't have to add it every time you need to use expressions inside `class=""` attributes.



Patch:
diff --git a/src/transformers/index.js b/src/transformers/index.js
index 61e3880..b7a840d 100644
--- a/src/transformers/index.js
+++ b/src/transformers/index.js
@@ -52,7 +52,7 @@ exports.addURLParams = (html, config) => addURLParams(html, config, true)
 exports.preventWidows = (html, config) => preventWidows(html, config)
 exports.replaceStrings = (html, config) => replaceStrings(html, config, true)
 exports.safeClassNames = (html, config) => safeClassNames(html, config, true)
-exports.removeUnusedCSS = (html, config) => removeUnusedCSS(html, config, true)
+exports.removeUnusedCSS = (html, config) => removeUnusedCSS(html, config)
 exports.removeAttributes = (html, config) => removeAttributes(html, config, true)
 exports.attributeToStyle = (html, config) => attributeToStyle(html, config, true)
 exports.removeInlineSizes = (html, config) => removeInlineSizes(html, config, true)
diff --git a/src/transformers/removeUnusedCss.js b/src/transformers/removeUnusedCss.js
index 531425a..8d58f80 100644
--- a/src/transformers/removeUnusedCss.js
+++ b/src/transformers/removeUnusedCss.js
@@ -1,13 +1,11 @@
 const {comb} = require('email-comb')
-const {get, isEmpty} = require('lodash')
+const {get, merge} = require('lodash')
 
-module.exports = async (html, config = {}, direct = false) => {
+module.exports = async (html, config = {}) => {
   if (get(config, 'removeUnusedCSS') === false) {
     return html
   }
 
-  const options = direct ? config : get(config, 'removeUnusedCSS', {})
-
   const safelist = [
     '*body*', // Gmail
     '.gmail*', // Gmail
@@ -26,15 +24,17 @@ module.exports = async (html, config = {}, direct = false) => {
     '.lang*' // Fenced code blocks
   ]
 
-  if (typeof options === 'boolean' && options) {
-    return comb(html, {whitelist: safelist}).result
+  const defaultOptions = {
+    backend: [
+      {heads: '{{', tails: '}}'},
+      {heads: '{%', tails: '%}'}
+    ],
+    whitelist: [...get(config, 'whitelist', []), ...safelist]
   }
 
-  if (!isEmpty(options)) {
-    options.whitelist = [...get(options, 'whitelist', []), ...safelist]
-
-    return comb(html, options).result
-  }
+  const options = typeof config === 'boolean' && config ?
+    defaultOptions :
+    merge(defaultOptions, get(config, 'removeUnusedCSS', config))
 
-  return html
+  return comb(html, options).result
 }


Test patch:
diff --git a/test/expected/posthtml/fetch.html b/test/expected/posthtml/fetch.html
deleted file mode 100644
index 24a1904..0000000
--- a/test/expected/posthtml/fetch.html
+++ /dev/null
@@ -1,5 +0,0 @@
-Leanne Graham
-
-Ervin Howell
-
-Clementine Bauch
diff --git a/test/fixtures/posthtml/component.html b/test/fixtures/posthtml/component.html
index 008d9ec..b39f7b9 100644
--- a/test/fixtures/posthtml/component.html
+++ b/test/fixtures/posthtml/component.html
@@ -5,7 +5,7 @@
     "foo": "bar"
   }'
 >
-Variable from page: [[ page.env ]]
+<p>Variable from page: [[ page.env ]]</p>
 
   <component
     src="test/stubs/components/component.html"
@@ -14,6 +14,6 @@ Variable from page: [[ page.env ]]
       "foo": "bar (nested)"
     }'
   >
-Variable from page (nested): [[ page.env ]]
+<p>Variable from page (nested): [[ page.env ]]</p>
   </component>
 </component>
diff --git a/test/fixtures/posthtml/fetch.html b/test/fixtures/posthtml/fetch.html
deleted file mode 100644
index 8b7d476..0000000
--- a/test/fixtures/posthtml/fetch.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<extends src="test/stubs/layouts/basic.html">
-  <block name="template">
-    <fetch url="test/stubs/data.json">
-      <each loop="user in response">
-[[ user.name ]]
-      </each>
-    </fetch>
-  </block>
-</extends>
diff --git a/test/stubs/breaking/bad.html b/test/stubs/breaking/bad.html
index 8ee73dc..b5ae15c 100644
--- a/test/stubs/breaking/bad.html
+++ b/test/stubs/breaking/bad.html
@@ -1,5 +1,5 @@
 ---
-title: [THIS] should break
+title: [THIS] should break the build
 ---
 
-<div>{{ page.title }}</div>
+<div class="inline">{{ page.title }}</div>
diff --git a/test/stubs/components/component.html b/test/stubs/components/component.html
index 2a30cc1..c65a4ea 100644
--- a/test/stubs/components/component.html
+++ b/test/stubs/components/component.html
@@ -1,5 +1,5 @@
-Variable from attribute: [[ text ]]
+<p>Variable from attribute: [[ text ]]</p>
 
-Variable from locals attribute: [[ foo ]]
+<p>Variable from locals attribute: [[ foo ]]</p>
 
 <content></content>
diff --git a/test/stubs/events/before-create.html b/test/stubs/events/before-create.html
index c1b26c7..783ed96 100644
--- a/test/stubs/events/before-create.html
+++ b/test/stubs/events/before-create.html
@@ -1,1 +1,1 @@
-Foo is {{ page.foo }}
+<div class="inline">Foo is {{ page.foo }}</div>
diff --git a/test/stubs/templates/1.html b/test/stubs/templates/1.html
index 1936cc1..d21b6f6 100644
--- a/test/stubs/templates/1.html
+++ b/test/stubs/templates/1.html
@@ -1,1 +1,1 @@
-html
+<div class="inline">html</div>
diff --git a/test/stubs/templates/2.html b/test/stubs/templates/2.html
new file mode 100644
index 0000000..5353a77
--- /dev/null
+++ b/test/stubs/templates/2.html
@@ -0,0 +1,1 @@
+<div class="inline">html modified</div>
\ No newline at end of file
diff --git a/test/stubs/templates/2.test b/test/stubs/templates/2.test
index 9daeafb..30d74d2 100644
--- a/test/stubs/templates/2.test
+++ b/test/stubs/templates/2.test
@@ -1,1 +1,1 @@
-test
+test
\ No newline at end of file
diff --git a/test/test-posthtml.js b/test/test-posthtml.js
index aecf02e..39d3525 100644
--- a/test/test-posthtml.js
+++ b/test/test-posthtml.js
@@ -1,17 +1,7 @@
 const test = require('ava')
-const Maizzle = require('../src')
+const {render} = require('../src')
 
-const path = require('path')
-const fs = require('fs')
-
-const readFile = (dir, filename) => fs.promises
-  .readFile(path.join(__dirname, dir, `${filename}.html`), 'utf8')
-  .then(html => html.trim())
-
-const fixture = file => readFile('fixtures', file)
-const expected = file => readFile('expected', file)
-
-const renderString = (string, options = {}) => Maizzle.render(string, options).then(({html}) => html)
+const renderString = (string, options = {}) => render(string, options).then(({html}) => html)
 
 test('layouts', async t => {
   const source = `---
@@ -58,7 +48,7 @@ test('components', async t => {
     "foo": "bar"
   }'
 >
-Variable from page: [[ page.env ]]
+<p class="hidden">Variable from page: [[ page.env ]]</p>
 
   <component
     src="test/stubs/components/component.html"
@@ -67,7 +57,7 @@ Variable from page: [[ page.env ]]
       "foo": "bar (nested)"
     }'
   >
-Variable from page (nested): [[ page.env ]]
+<p>Variable from page (nested): [[ page.env ]]</p>
   </component>
 </component>`
 
@@ -86,23 +76,23 @@ Variable from page (nested): [[ page.env ]]
 
   const html = await renderString(source, options)
 
-  t.is(html.trim(), `Variable from attribute: Example
-
-Variable from locals attribute: bar
-
-
-Variable from page: prod
-
-  Variable from attribute: Nested component
-
-Variable from locals attribute: bar (nested)
-
-
-Variable from page (nested): prod`)
+  t.is(html.trim(), `<p>Variable from attribute: Example</p>
+<p>Variable from locals attribute: bar</p><p class="hidden">Variable from page: prod</p>
+  <p>Variable from attribute: Nested component</p>
+<p>Variable from locals attribute: bar (nested)</p><p>Variable from page (nested): prod</p>`)
 })
 
 test('fetch component', async t => {
-  const source = await fixture('posthtml/fetch')
+  const source = `<extends src="test/stubs/layouts/basic.html">
+  <block name="template">
+    <fetch url="test/stubs/data.json">
+      <each loop="user in response">
+[[ user.name + (loop.last ? '' : ', ') ]]
+      </each>
+    </fetch>
+  </block>
+</extends>`
+
   const options = {
     maizzle: {
       env: 'maizzle-ci',
@@ -116,8 +106,7 @@ test('fetch component', async t => {
     }
   }
 
-  let html = await renderString(source, options)
-  html = html.replace(/[^\S\r\n]+$/gm, '')
+  const html = await renderString(source, options)
 
-  t.is(html.trim(), await expected('posthtml/fetch'))
+  t.is(html.trim(), 'Leanne Graham, Ervin Howell, Clementine Bauch')
 })
diff --git a/test/test-tailwindcss.js b/test/test-tailwindcss.js
index ba6f2ed..c38c5df 100644
--- a/test/test-tailwindcss.js
+++ b/test/test-tailwindcss.js
@@ -3,7 +3,7 @@ const Tailwind = require('../src/generators/tailwindcss')
 
 test('throws on compile error', async t => {
   await t.throwsAsync(async () => {
-    await Tailwind.compile('.test {@apply inexistent;}', '<div class="test">Test</a>', {}, {})
+    await Tailwind.compile('div {@apply inexistent;}', '<div class="inline">Test</a>', {}, {})
   }, {instanceOf: SyntaxError})
 })
 
@@ -51,7 +51,7 @@ test('works with custom `content` sources', async t => {
 test('works with custom `files` sources', async t => {
   const css = await Tailwind.compile(
     '@tailwind utilities;',
-    '<div></div>',
+    '<div class="inline"></div>',
     {
       content: {
         files: ['./test/stubs/tailwind/*.*']
@@ -65,7 +65,7 @@ test('works with custom `files` sources', async t => {
 test('uses maizzle template path as content source', async t => {
   const css = await Tailwind.compile(
     '@tailwind utilities;',
-    '<div></div>',
+    '<div class="inline"></div>',
     {},
     {
       build: {
@@ -82,7 +82,7 @@ test('uses maizzle template path as content source', async t => {
 test('uses maizzle template path as content source (single file)', async t => {
   const css = await Tailwind.compile(
     '@tailwind utilities;',
-    '<div></div>',
+    '<div class="inline"></div>',
     {},
     {
       build: {
@@ -108,8 +108,8 @@ test('uses custom postcss plugins from the maizzle config', async t => {
     }
   }
 
-  const css = await Tailwind.compile('.test {transform: scale(0.5)}', '<div class="test">Test</a>', {}, maizzleConfig)
+  const css = await Tailwind.compile('.test {transform: scale(0.5)}', '<div class="test inline">Test</a>', {}, maizzleConfig)
 
   t.not(css, undefined)
-  t.is(css.trim(), '.test {-webkit-transform: scale(0.5);transform: scale(0.5)}')
+  t.is(css.trim(), '.inline {display: inline !important} .test {-webkit-transform: scale(0.5);transform: scale(0.5)}')
 })
diff --git a/test/test-todisk.js b/test/test-todisk.js
index 56e61c1..134b150 100644
--- a/test/test-todisk.js
+++ b/test/test-todisk.js
@@ -30,11 +30,6 @@ test('skips if no templates found', async t => {
         destination: {
           path: t.context.folder
         }
-      },
-      tailwind: {
-        config: {
-          purge: false
-        }
       }
     }
   })
@@ -68,8 +63,8 @@ test('outputs files at the correct location', async t => {
   })
 
   t.true(await fs.pathExists(t.context.folder))
-  t.is(string.length, 2)
-  t.is(array.length, 2)
+  t.is(string.length, 3)
+  t.is(array.length, 3)
 })
 
 test('outputs files at the correct location if multiple template sources are used', async t => {
@@ -94,7 +89,7 @@ test('outputs files at the correct location if multiple template sources are use
   })
 
   t.true(await fs.pathExists(t.context.folder))
-  t.is(files.length, 5)
+  t.is(files.length, 6)
 })
 
 test('copies all files in the `filetypes` option to destination', async t => {
@@ -107,11 +102,6 @@ test('copies all files in the `filetypes` option to destination', async t => {
         destination: {
           path: t.context.folder
         }
-      },
-      tailwind: {
-        config: {
-          purge: false
-        }
       }
     }
   })
@@ -130,11 +120,6 @@ test('outputs files with the correct extension', async t => {
           path: t.context.folder,
           extension: 'blade.php'
         }
-      },
-      tailwind: {
-        config: {
-          purge: false
-        }
       }
     }
   })
@@ -240,11 +225,6 @@ test('copies assets to destination', async t => {
           source: 'test/stubs/assets',
           destination: 'images'
         }
-      },
-      tailwind: {
-        config: {
-          purge: false
-        }
       }
     }
   })
@@ -264,11 +244,6 @@ test('runs the `beforeCreate` event', async t => {
         destination: {
           path: t.context.folder
         }
-      },
-      tailwind: {
-        config: {
-          purge: false
-        }
       }
     },
     events: {
@@ -281,7 +256,7 @@ test('runs the `beforeCreate` event', async t => {
   const filename = await fs.readdir(t.context.folder)
   const html = await fs.readFile(`${t.context.folder}/${filename[0]}`, 'utf8')
 
-  t.is(html.trim(), 'Foo is bar')
+  t.is(html.trim(), '<div class="inline">Foo is bar</div>')
 })
 
 test('runs the `afterBuild` event', async t => {
@@ -293,11 +268,6 @@ test('runs the `afterBuild` event', async t => {
         destination: {
           path: t.context.folder
         }
-      },
-      tailwind: {
-        config: {
-          purge: false
-        }
       }
     },
     events: {
@@ -325,11 +295,6 @@ test('supports multiple asset paths', async t => {
           source: ['test/stubs/assets', 'test/stubs/plaintext', 'test/stubs/invalid'],
           destination: 'extras'
         }
-      },
-      tailwind: {
-        config: {
-          purge: false
-        }
       }
     }
   })
@@ -347,11 +312,6 @@ test('warns if a template cannot be rendered and `fail` option is undefined', as
         destination: {
           path: t.context.folder
         }
-      },
-      tailwind: {
-        config: {
-          purge: false
-        }
       }
     }
   })
@@ -368,11 +328,6 @@ test('warns if a template cannot be rendered and `fail` option is `verbose`', as
         destination: {
           path: t.context.folder
         }
-      },
-      tailwind: {
-        config: {
-          purge: false
-        }
       }
     }
   })
@@ -389,11 +344,6 @@ test('warns if a template cannot be rendered and `fail` option is `silent`', asy
         destination: {
           path: t.context.folder
         }
-      },
-      tailwind: {
-        config: {
-          purge: false
-        }
       }
     }
   })
@@ -428,12 +378,12 @@ test('local server does not compile unwanted file types', async t => {
   t.true(await fs.pathExists(`${t.context.folder}/2.test`))
 
   // Tests watching changes to files
-  await fs.outputFile('test/stubs/templates/1.html', 'html\n')
-  t.is(await fs.readFile('test/stubs/templates/1.html', 'utf8'), 'html\n')
+  await fs.outputFile('test/stubs/templates/2.html', '<div class="inline">html modified</div>')
+  t.is(await fs.readFile('test/stubs/templates/2.html', 'utf8'), '<div class="inline">html modified</div>')
 
-  // Don't trigger rebuilds on files not in filetypes
-  await fs.outputFile('test/stubs/templates/2.test', 'test\n')
-  t.is(await fs.readFile('test/stubs/templates/2.test', 'utf8'), 'test\n')
+  // Don't trigger rebuilds on files not in `filetypes`
+  await fs.outputFile('test/stubs/templates/2.test', 'test')
+  t.is(await fs.readFile('test/stubs/templates/2.test', 'utf8'), 'test')
 })
 
 test('throws if it cannot spin up local development server', async t => {
@@ -456,7 +406,7 @@ test('works with templates.source defined as function (string paths)', async t =
   })
 
   t.true(await fs.pathExists(t.context.folder))
-  t.is(files.length, 2)
+  t.is(files.length, 3)
 })
 
 test('works with templates.source defined as function (array paths)', async t => {
@@ -476,7 +426,7 @@ test('works with templates.source defined as function (array paths)', async t =>
   })
 
   t.true(await fs.pathExists(t.context.folder))
-  t.is(files.length, 2)
+  t.is(files.length, 3)
 })
 
 test('throws if templates path is invalid', async t => {
@@ -535,9 +485,9 @@ test('sets config.build.current.path', async t => {
     path: {
       root: '',
       dir: t.context.folder,
-      base: '1.html',
+      base: '2.html',
       ext: '.html',
-      name: '1'
+      name: '2'
     }
   })
 })
diff --git a/test/test-tostring.js b/test/test-tostring.js
index e86798a..bc1f7ec 100644
--- a/test/test-tostring.js
+++ b/test/test-tostring.js
@@ -4,7 +4,7 @@ const Maizzle = require('../src')
 const renderString = (string, options = {}) => Maizzle.render(string, options).then(({html}) => html)
 
 test('uses environment config file(s) if available', async t => {
-  const source = `<div>{{ page.mail }}</div>`
+  const source = `<div class="inline">{{ page.mail }}</div>`
 
   const html = await renderString(source, {
     maizzle: {
@@ -12,7 +12,7 @@ test('uses environment config file(s) if available', async t => {
     }
   })
 
-  t.is(html, '<div>puzzle</div>')
+  t.is(html, '<div class="inline">puzzle</div>')
 })
 
 test('throws if first argument is not an HTML string', async t => {
@@ -28,7 +28,7 @@ test('throws if first argument is an empty string', async t => {
 })
 
 test('runs the `beforeRender` event', async t => {
-  const html = await renderString(`<div>{{ page.foo }}</div>`, {
+  const html = await renderString(`<div class="inline">{{ page.foo }}</div>`, {
     beforeRender(html, config) {
       config.foo = 'bar'
 
@@ -36,11 +36,11 @@ test('runs the `beforeRender` event', async t => {
     }
   })
 
-  t.is(html, `<div>bar</div>`)
+  t.is(html, `<div class="inline">bar</div>`)
 })
 
 test('runs the `afterRender` event', async t => {
-  const result = await renderString(`<div>foo</div>`, {
+  const result = await renderString(`<div class="inline">foo</div>`, {
     afterRender(html, config) {
       config.replaceStrings = {
         foo: 'baz'
@@ -50,11 +50,11 @@ test('runs the `afterRender` event', async t => {
     }
   })
 
-  t.is(result, `<div>baz</div>`)
+  t.is(result, `<div class="inline">baz</div>`)
 })
 
 test('runs the `afterTransformers` event', async t => {
-  const result = await renderString(`<div>foo</div>`, {
+  const result = await renderString(`<div class="inline">foo</div>`, {
     maizzle: {
       title: 'bar'
     },
@@ -63,11 +63,11 @@ test('runs the `afterTransformers` event', async t => {
     }
   })
 
-  t.is(result, `<div>bar</div>`)
+  t.is(result, `<div class="inline">bar</div>`)
 })
 
 test('locals work when defined in all supported places', async t => {
-  const result = await renderString(`{{ page.one }}, {{ two }}, {{ three }}`, {
+  const result = await renderString(`{{ page.one }}, {{ two }}, {{ three }}, {{ inline }}`, {
     maizzle: {
       one: 1,
       build: {
@@ -85,11 +85,11 @@ test('locals work when defined in all supported places', async t => {
     }
   })
 
-  t.is(result, `1, 2, 3`)
+  t.is(result, `1, 2, 3, undefined`)
 })
 
 test('prevents overwriting page object', async t => {
-  const result = await renderString(`{{ page.one }}, {{ two }}, {{ three }}`, {
+  const result = await renderString(`{{ page.one }}, {{ two }}, {{ three }}, {{ inline }}`, {
     maizzle: {
       one: 1,
       build: {
@@ -111,7 +111,7 @@ test('prevents overwriting page object', async t => {
     }
   })
 
-  t.is(result, `1, undefined, undefined`)
+  t.is(result, `1, undefined, undefined, undefined`)
 })
 
 test('preserves css in marked style tags (tailwindcss)', async t => {
@@ -124,6 +124,9 @@ test('preserves css in marked style tags (tailwindcss)', async t => {
         [data-ogsc] .inexistent {
           color: #ef4444;
         }
+        [data-ogsc] .ogsc:hidden {
+          display: none;
+        }
         div > u + .body .gmail-android-block {
           display: block !important;
         }
@@ -144,16 +147,18 @@ test('preserves css in marked style tags (tailwindcss)', async t => {
     }
   })
 
-  t.true(html.includes('[data-ogsc] .inexistent'))
+  t.true(html.includes('[data-ogsc] .ogsc:hidden'))
+  t.false(html.includes('[data-ogsc] .inexistent'))
+
   t.true(html.includes('div > u + .body .gmail-android-block'))
   t.true(html.includes('u + #body a'))
 })
 
 test('@import css files in marked style tags', async t => {
-  const source = `<style postcss>@import "test/stubs/post.css";</style>`
+  const source = `<style class="inline" postcss>@import "test/stubs/post.css";</style>`
   const html = await renderString(source)
 
-  t.is(html, `<style>div {
+  t.is(html, `<style class="inline">div {
   margin: 1px 2px 3px 4px;
 }</style>`)
 })
diff --git a/test/test-transformers.js b/test/test-transformers.js
index 1aea4b2..46af063 100644
--- a/test/test-transformers.js
+++ b/test/test-transformers.js
@@ -94,7 +94,7 @@ test('remove unused CSS', async t => {
       </style>
     </head>
     <body>
-      <div class="foo">test div with some text</div>
+      <div class="foo {{ test }}">test div with some text</div>
     </body>
   </html>`
 
@@ -107,7 +107,7 @@ test('remove unused CSS', async t => {
       </style>
     </head>
     <body>
-      <div class="foo">test div with some text</div>
+      <div class="foo {{ test }}">test div with some text</div>
     </body>
   </html>`
 
@@ -119,15 +119,17 @@ test('remove unused CSS', async t => {
       </style>
     </head>
     <body>
-      <div class="foo">test div with some text</div>
+      <div class="foo {{ test }}">test div with some text</div>
     </body>
   </html>`
 
-  const withOptions = await Maizzle.removeUnusedCSS(html, {whitelist: ['.bar*']})
   const enabled = await Maizzle.removeUnusedCSS(html, true)
+  const disabled = await Maizzle.removeUnusedCSS(html, {removeUnusedCSS: false})
+  const withOptions = await Maizzle.removeUnusedCSS(html, {whitelist: ['.bar*']})
 
-  t.is(withOptions, result1)
   t.is(enabled, result2)
+  t.is(disabled, html)
+  t.is(withOptions, result1)
 })
 
 test('remove unused CSS (disabled)', async t => {