Repository: maizzle/framework (js)

Issue:
[DOCS] Top level `plaintext` config does not work in 4.0.0
I upgraded from v3 to v4, the top level `plaintext: true` does not generate plain text templates anymore.

I had to put the plaintext object in `build.templates` to make it work. The documentation still points to a top-level `plaintext` config.

Before:

```js
module.exports = {
  plaintext: true,
  build: {
    templates: {
      destination: {
        path: '../backend/templates/maizzle',
      },
    },
  },
  inlineCSS: true,
  removeUnusedCSS: true,
}

```

After:
```js
module.exports = {
  build: {
    templates: {
      destination: {
        path: '../backend/templates/maizzle',
      },
      plaintext: {
        destination: {
          path: '../backend/templates/maizzle',
        },
      },
    },
  },
  inlineCSS: true,
  removeUnusedCSS: true
}

```

- Maizzle Version: 4.0.0
- Node.js Version: 14.19.3



Pull request description:
Update plaintext functionality
### New features

#### `<not-plaintext>`

Added a `<not-plaintext>` tag that you can use to discard content from a plaintext version while keeping it in the HTML.

For example:

```html
<div>Show everywhere</div>
<plaintext>Show in plaintext</plaintext>
<not-plaintext>
  <p>Show only in HTML</p>
</not-plaintext>
```

Plaintext:

```
Show everywhere
Show in plaintext
```

HTML:

```html
<div>Show everywhere</div>
<p>Show only in HTML</p>
```

### Fixes

- fixed an issue with `plaintext: true` in a template's Front Matter not generating the plaintext file (closes #707)

Patch:
diff --git a/src/generators/output/to-disk.js b/src/generators/output/to-disk.js
index b790f88..aecf70e 100644
--- a/src/generators/output/to-disk.js
+++ b/src/generators/output/to-disk.js
@@ -3,7 +3,6 @@ const fs = require('fs-extra')
 const glob = require('glob-promise')
 const {get, isEmpty, merge} = require('lodash')
 const {asyncForEach} = require('../../utils/helpers')
-const removePlaintextTags = require('../../transformers/plaintext')
 
 const Config = require('../config')
 const Tailwind = require('../tailwindcss')
@@ -123,7 +122,8 @@ module.exports = async (env, spinner, config) => {
                * tags from the markup before outputting the file.
                */
 
-              const plaintextConfig = get(templateConfig, 'plaintext')
+              // Check if plaintext: true globally, fallback to template's front matter
+              const plaintextConfig = get(templateConfig, 'plaintext', get(compiled.config, 'plaintext', false))
               const plaintextPath = get(plaintextConfig, 'destination.path', config.permalink || file)
 
               if (Boolean(plaintextConfig) || !isEmpty(plaintextConfig)) {
@@ -133,11 +133,12 @@ module.exports = async (env, spinner, config) => {
                     plaintextPath,
                     merge(plaintextConfig, {filepath: file})
                   )
-                  .then(({plaintext, destination}) => fs.outputFile(destination, plaintext))
+                  .then(async ({html, plaintext, destination}) => {
+                    compiled.html = html
+                    await fs.outputFile(destination, plaintext)
+                  })
               }
 
-              compiled.html = removePlaintextTags(compiled.html, config)
-
               /**
                * Output file
                */
diff --git a/src/generators/plaintext.js b/src/generators/plaintext.js
index d0c391d..8379082 100644
--- a/src/generators/plaintext.js
+++ b/src/generators/plaintext.js
@@ -1,49 +1,84 @@
 const path = require('path')
 const {get} = require('lodash')
+const posthtml = require('posthtml')
 const {stripHtml} = require('string-strip-html')
 
-module.exports.generate = async (html, destination, config) => {
-  const configDestinationPath = get(config, 'destination.path')
-  const extension = get(config, 'destination.extension', 'txt')
+const self = {
+  handleCustomTags: (html, config = {}) => {
+    const posthtmlOptions = get(config, 'build.posthtml.options', {})
 
-  const plaintext = stripHtml(html, {
-    dumpLinkHrefsNearby: {
-      enabled: true
-    },
-    ...get(config, 'options', {})
-  }).result
+    const posthtmlPlugin = () => tree => {
+      const process = node => {
+        if (node.tag === 'plaintext') {
+          return {
+            tag: false,
+            content: ['']
+          }
+        }
 
-  // If we set plaintext.destination.path in config/fm
-  if (configDestinationPath) {
-    /**
-     * Using a file path will generate a single plaintext file,
-     * no matter how many templates there are.
-     *
-     * It will be based on the last-processed template.
-     */
-    if (path.extname(configDestinationPath)) {
-      destination = configDestinationPath
+        if (node.tag === 'not-plaintext') {
+          return {
+            tag: false,
+            content: tree.render(node.content)
+          }
+        }
+
+        return node
+      }
+
+      return tree.walk(process)
+    }
+
+    return posthtml([posthtmlPlugin()]).process(html, {...posthtmlOptions, sync: true}).html
+  },
+  generate: async (html, destination, config) => {
+    const configDestinationPath = get(config, 'destination.path')
+    const extension = get(config, 'destination.extension', 'txt')
+
+    const plaintext = stripHtml(html, {
+      dumpLinkHrefsNearby: {
+        enabled: true
+      },
+      stripTogetherWithTheirContents: ['script', 'style', 'xml', 'not-plaintext'],
+      ...get(config, 'options', {})
+    }).result
+
+    html = self.handleCustomTags(html, config)
 
-      return {plaintext, destination}
+    // If we set plaintext.destination.path in config/fm
+    if (configDestinationPath) {
+      /**
+       * Using a file path will generate a single plaintext file,
+       * no matter how many templates there are.
+       *
+       * It will be based on the last-processed template.
+       */
+      if (path.extname(configDestinationPath)) {
+        destination = configDestinationPath
+
+        return {html, plaintext, destination}
+      }
+
+      /**
+       * Using a directory-like path for plaintext.destination.path
+       */
+      destination = path.join(configDestinationPath, path.basename(config.filepath, path.extname(config.filepath)) + '.' + extension)
+
+      return {html, plaintext, destination}
     }
 
     /**
-     * Using a directory-like path for plaintext.destination.path
+     * Use template's `permalink` Front Matter key,
+     * fall back to the original `destination`.
      */
-    destination = path.join(configDestinationPath, path.basename(config.filepath, path.extname(config.filepath)) + '.' + extension)
-
-    return {plaintext, destination}
-  }
+    destination = get(config, 'permalink', destination)
 
-  /**
-   * Use template's `permalink` Front Matter key,
-   * fall back to the original `destination`.
-   */
-  destination = get(config, 'permalink', destination)
+    if (typeof destination === 'string') {
+      destination = path.join(path.dirname(destination), path.basename(destination, path.extname(destination)) + '.' + extension)
+    }
 
-  if (typeof destination === 'string') {
-    destination = path.join(path.dirname(destination), path.basename(destination, path.extname(destination)) + '.' + extension)
+    return {html, plaintext, destination}
   }
-
-  return {plaintext, destination}
 }
+
+module.exports = self
diff --git a/src/transformers/plaintext.js b/src/transformers/plaintext.js
deleted file mode 100644
index 2873aea..0000000
--- a/src/transformers/plaintext.js
+++ /dev/null
@@ -1,23 +0,0 @@
-const {get} = require('lodash')
-const posthtml = require('posthtml')
-
-module.exports = (html, config = {}) => {
-  const posthtmlOptions = get(config, 'build.posthtml.options', {})
-
-  return posthtml([plaintext()]).process(html, {...posthtmlOptions, sync: true}).html
-}
-
-const plaintext = () => tree => {
-  const process = node => {
-    if (node.tag === 'plaintext') {
-      return {
-        tag: false,
-        content: ['']
-      }
-    }
-
-    return node
-  }
-
-  return tree.walk(process)
-}


Test patch:
diff --git a/test/stubs/plaintext/front-matter.html b/test/stubs/plaintext/front-matter.html
new file mode 100644
index 0000000..1677da2
--- /dev/null
+++ b/test/stubs/plaintext/front-matter.html
@@ -0,0 +1,9 @@
+---
+plaintext: true
+---
+
+<div>Show in HTML</div>
+<plaintext>Show in plaintext</plaintext>
+<not-plaintext>
+  <table><tr><td>Remove from plaintext</td></tr></table>
+</not-plaintext>
diff --git a/test/stubs/plaintext/plaintext.html b/test/stubs/plaintext/plaintext.html
index 3c6dff6..fec1e0b 100644
--- a/test/stubs/plaintext/plaintext.html
+++ b/test/stubs/plaintext/plaintext.html
@@ -1,2 +1,5 @@
 <div>Show in HTML</div>
 <plaintext>Show in plaintext</plaintext>
+<not-plaintext>
+  <table><tr><td>Remove from plaintext</td></tr></table>
+</not-plaintext>
diff --git a/test/test-todisk.js b/test/test-todisk.js
index d0b8597..0cea9cd 100644
--- a/test/test-todisk.js
+++ b/test/test-todisk.js
@@ -94,7 +94,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, 3)
+  t.is(files.length, 5)
 })
 
 test('copies all files in the `filetypes` option to destination', async t => {
@@ -154,23 +154,49 @@ test('outputs plaintext files', async t => {
           path: t.context.folder
         },
         plaintext: true
-      },
-      tailwind: {
-        config: {
-          purge: false
+      }
+    },
+    extraAttributes: false
+  })
+
+  t.true(files.includes(`${t.context.folder}/plaintext.txt`))
+
+  t.is(
+    await fs.readFile(`${t.context.folder}/plaintext.txt`, 'utf8'),
+    'Show in HTML\nShow in plaintext'
+  )
+
+  t.is(
+    await fs.readFile(`${t.context.folder}/plaintext.html`, 'utf8'),
+    '<div>Show in HTML</div>\n\n\n  <table><tr><td>Remove from plaintext</td></tr></table>\n\n'
+  )
+})
+
+test('outputs plaintext files (front matter)', async t => {
+  const {files} = await Maizzle.build('maizzle-ci', {
+    fail: 'silent',
+    build: {
+      templates: {
+        source: 'test/stubs/plaintext',
+        destination: {
+          path: t.context.folder
         }
       }
-    }
+    },
+    extraAttributes: false
   })
 
-  const plaintext = files.filter(file => file.includes('.txt'))
-  const html = files.filter(file => file.includes('.html'))
-  const plaintextContent = await fs.readFile(plaintext[0], 'utf8')
-  const htmlContent = await fs.readFile(html[0], 'utf8')
+  t.true(files.includes(`${t.context.folder}/front-matter.txt`))
 
-  t.is(plaintext[0], `${t.context.folder}/plaintext.txt`)
-  t.is(plaintextContent, 'Show in HTML\nShow in plaintext')
-  t.is(htmlContent, '<div>Show in HTML</div>\n\n')
+  t.is(
+    await fs.readFile(`${t.context.folder}/front-matter.txt`, 'utf8'),
+    'Show in HTML\nShow in plaintext'
+  )
+
+  t.is(
+    await fs.readFile(`${t.context.folder}/front-matter.html`, 'utf8'),
+    '<div>Show in HTML</div>\n\n\n  <table><tr><td>Remove from plaintext</td></tr></table>\n\n'
+  )
 })
 
 test('outputs plaintext files (custom path)', async t => {
@@ -187,18 +213,11 @@ test('outputs plaintext files (custom path)', async t => {
             path: `${t.context.folder}/nested/plain.text`
           }
         }
-      },
-      tailwind: {
-        config: {
-          purge: false
-        }
       }
     }
   })
 
-  const plaintext = files.filter(file => file.includes('.text'))
-
-  t.is(plaintext[0], `${t.context.folder}/nested/plain.text`)
+  t.true(files.includes(`${t.context.folder}/nested/plain.text`))
 })
 
 test('renders plaintext string', async t => {
diff --git a/test/test-transformers.js b/test/test-transformers.js
index 81a4dff..1519483 100644
--- a/test/test-transformers.js
+++ b/test/test-transformers.js
@@ -1,6 +1,5 @@
 const test = require('ava')
 const Maizzle = require('../src')
-const removePlaintextTags = require('../src/transformers/plaintext')
 
 const path = require('path')
 const fs = require('fs')
@@ -232,13 +231,6 @@ test('minify (disabled)', async t => {
   t.is(html, '<div>\n\n<p>\n\ntest</p></div>')
 })
 
-test('removes plaintext tag', t => {
-  let html = removePlaintextTags('<plaintext>Removed</plaintext><div>Preserved</div>')
-  html = html.replace(/[^\S\r\n]+$/gm, '').trim()
-
-  t.is(html, '<div>Preserved</div>')
-})
-
 test('replace strings', async t => {
   const html = await Maizzle.replaceStrings('initial text', {initial: 'updated'})