Repository: tailwindlabs/tailwindcss (ts)

Issue:
JIT includes quotes in class names when classes are followed by `'` and `]` characters
### What version of Tailwind CSS are you using?

v2.2.4

### What build tool (or framework if it abstracts the build tool) are you using?

Tailwind CLI

### What version of Node.js are you using?

v16.2.0

### What browser are you using?

Firefox (v89.0.2)

### What operating system are you using?

macOS (11.4)

### Reproduction repository

https://github.com/bakerkretzmar/tailwindcss-jit-quotes-repro

### Describe your issue

If an arbitrary value in a class name is immediately followed by a single quote mark and another closing bracket, the quote and bracket are included in the generated class name.

For example, this markup:

```html
<h1 class="<?php echo wrap(['class' => 'max-w-[16rem]']); ?>">Hello world</h1>
```

generates this CSS:

```css
.max-w-\[16rem\]\'\] {
  max-width: 16rem]'
}
```

The string `['class' => 'max-w-[16rem]']` seems to be the issue, specifically the fact that it ends with `]']`. It looks like the JIT engine stops at the **second** `]`, interpreting the arbitrary value for the CSS property to be `16rem]'`.

Adding a space before those characters (`['class' => 'max-w-[16rem] ']`) 'fixes' the issue and produces valid CSS.

Pull request description:
Improve extractor for arbitrary values with quotes
Fixes #4801.

This PR improves our default extractor regex to handle arbitrary values that are wrapped in quotes more precisely.

Previously, given this input:

```html
<h1 class="<?php echo wrap(['class' => 'max-w-[16rem]']); ?>">Hello world</h1>
```

...we would find a class named `max-w-[16rem]']`. This PR makes sure we find `max-w-[16rem]` instead, while preserving the ability to match things like `content-['hello']` and even `content-[']']`.

We don't match this properly yet: `content-['hello[]']`, but we also didn't before this PR, so this is still purely an improvement over what was there before.

Since the regex is getting long, I've also broken it up into 4 parts with a few comments for each piece to explain what it's for.

Patch:
diff --git a/src/jit/lib/expandTailwindAtRules.js b/src/jit/lib/expandTailwindAtRules.js
index 12abf8fa..c7db3a1e 100644
--- a/src/jit/lib/expandTailwindAtRules.js
+++ b/src/jit/lib/expandTailwindAtRules.js
@@ -6,7 +6,13 @@ import cloneNodes from '../../util/cloneNodes'
 let env = sharedState.env
 let contentMatchCache = sharedState.contentMatchCache
 
-const BROAD_MATCH_GLOBAL_REGEXP = /([^<>"'`\s]*\[[^<>\s]+\])|([^<>"'`\s]*[^<>"'`\s:])/g
+const PATTERNS = [
+  "([^<>\"'`\\s]*\\['[^<>\"'`\\s]*'\\])", // `content-['hello']` but not `content-['hello']']`
+  '([^<>"\'`\\s]*\\["[^<>"\'`\\s]*"\\])', // `content-["hello"]` but not `content-["hello"]"]`
+  '([^<>"\'`\\s]*\\[[^<>"\'`\\s]+\\])', // `fill-[#bada55]`
+  '([^<>"\'`\\s]*[^<>"\'`\\s:])', // `px-1.5`, `uppercase` but not `uppercase:`
+].join('|')
+const BROAD_MATCH_GLOBAL_REGEXP = new RegExp(PATTERNS, 'g')
 const INNER_MATCH_GLOBAL_REGEXP = /[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g
 
 const builtInExtractors = {
@@ -85,7 +91,7 @@ function getClassCandidates(content, extractor, contentMatchCache, candidates, s
         candidates.add(match)
       }
     } else {
-      let extractorMatches = extractor(line)
+      let extractorMatches = extractor(line).filter((s) => s !== '!*')
       let lineMatchesSet = new Set(extractorMatches)
 
       for (let match of lineMatchesSet) {


Test patch:
diff --git a/tests/jit/extractor-edge-cases.test.js b/tests/jit/extractor-edge-cases.test.js
new file mode 100644
index 00000000..88fac0c1
--- /dev/null
+++ b/tests/jit/extractor-edge-cases.test.js
@@ -0,0 +1,57 @@
+import postcss from 'postcss'
+import path from 'path'
+import tailwind from '../../src/jit/index.js'
+
+function run(input, config = {}) {
+  const { currentTestName } = expect.getState()
+
+  return postcss(tailwind(config)).process(input, {
+    from: `${path.resolve(__filename)}?test=${currentTestName}`,
+  })
+}
+
+test('PHP arrays', async () => {
+  let config = {
+    mode: 'jit',
+    purge: [
+      {
+        raw: `<h1 class="<?php echo wrap(['class' => "max-w-[16rem]"]); ?>">Hello world</h1>`,
+      },
+    ],
+    theme: {},
+    plugins: [],
+  }
+
+  let css = `@tailwind utilities`
+
+  return run(css, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(`
+      .max-w-\\[16rem\\] {
+        max-width: 16rem;
+      }
+    `)
+  })
+})
+
+test('arbitrary values with quotes', async () => {
+  let config = {
+    mode: 'jit',
+    purge: [
+      {
+        raw: `<div class="content-['hello]']"></div>`,
+      },
+    ],
+    theme: {},
+    plugins: [],
+  }
+
+  let css = `@tailwind utilities`
+
+  return run(css, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(`
+      .content-\\[\\'hello\\]\\'\\] {
+        content: 'hello]';
+      }
+    `)
+  })
+})
diff --git a/tests/jit/important-modifier-prefix.test.html b/tests/jit/important-modifier-prefix.test.html
index 8124d78c..03189c56 100644
--- a/tests/jit/important-modifier-prefix.test.html
+++ b/tests/jit/important-modifier-prefix.test.html
@@ -1,3 +1,5 @@
+<!-- The string "!*" can cause problems if we don't handle it, let's include it -->
+<div class="!*"></div>
 <div class="!tw-container"></div>
 <div class="!tw-font-bold"></div>
 <div class="hover:!tw-text-center"></div>