Repository: tailwindlabs/tailwindcss (ts)

Issue:
Update default extractor for purgeCSS to account for escaped class names
### Describe the problem:

Not sure if this is a bug or a feature request, but I just noticed that the default extractor for purgeCSS does not extract class names that have been escaped. For example, in translation JSON files, we have a need to include html elements as such:

```
{
  "helloThere": "Hello there, <span class=\"whitespace-no-wrap\">Mr. Jones</span>"
}
```

But it seems like the patterns used in the default extractor don't collect these class names properly. Using a Regex tester, it looks like the final `\` is also included:

![image](https://user-images.githubusercontent.com/1256477/92688835-ffb68c00-f378-11ea-8b2b-e7698fbafae1.png)

I guess it's not too much of a problem, as one can just add another extractor for JSON files. But it took as a while to figure out the work around, so it might help others in the future if the patterns could also account for escaped strings.

Pull request:
Safeguard against trailing slash in purge matches
Resolves #2360.

This PR adds another set of matches for purge to consider which are the original broad matches, minus any trailing slashes. This means in this string:

```
class=\"foo\"
```

...both `foo\` and `foo` will be checked in the stylesheet.

Patch:
diff --git a/src/lib/purgeUnusedStyles.js b/src/lib/purgeUnusedStyles.js
index 5b0404ae..5cd67a93 100644
--- a/src/lib/purgeUnusedStyles.js
+++ b/src/lib/purgeUnusedStyles.js
@@ -112,14 +112,17 @@ export default function purgeUnusedUtilities(config, configChanged) {
       defaultExtractor: content => {
         // Capture as liberally as possible, including things like `h-(screen-1.5)`
         const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || []
+        const broadMatchesWithoutTrailingSlash = broadMatches.map(match => _.trimEnd(match, '\\'))
 
         // Capture classes within other delimiters like .block(class="w-1/2") in Pug
         const innerMatches = content.match(/[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g) || []
 
+        const matches = broadMatches.concat(broadMatchesWithoutTrailingSlash).concat(innerMatches)
+
         if (_.get(config, 'purge.preserveHtmlElements', true)) {
-          return [...htmlTags].concat(broadMatches).concat(innerMatches)
+          return [...htmlTags].concat(matches)
         } else {
-          return broadMatches.concat(innerMatches)
+          return matches
         }
       },
       ...config.purge.options,


Test patch:
diff --git a/__tests__/fixtures/purge-example.html b/__tests__/fixtures/purge-example.html
index 50ffdc3e..1ee886ad 100644
--- a/__tests__/fixtures/purge-example.html
+++ b/__tests__/fixtures/purge-example.html
@@ -37,3 +37,8 @@ span.inline-grid.grid-cols-3(class="px-1.5")
     %samp.font-mono{:data-foo => "bar"}= output
     .col-span-4[aria-hidden=true]
     .tracking-tight#headline
+
+<!-- JSON -->
+{
+  "helloThere": "Hello there, <span class=\"whitespace-no-wrap\">Mr. Jones</span>"
+}
\ No newline at end of file
diff --git a/__tests__/purgeUnusedStyles.test.js b/__tests__/purgeUnusedStyles.test.js
index dd6f7f3c..093efbb5 100644
--- a/__tests__/purgeUnusedStyles.test.js
+++ b/__tests__/purgeUnusedStyles.test.js
@@ -90,7 +90,7 @@ function assertPurged(result) {
   expect(result.css).toContain('.font-mono')
   expect(result.css).toContain('.col-span-4')
   expect(result.css).toContain('.tracking-tight')
-  expect(result.css).toContain('.tracking-tight')
+  expect(result.css).toContain('.whitespace-no-wrap')
 }
 
 test('purges unused classes', () => {