Repository: tailwindlabs/tailwindcss (TypeScript)

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.


Hints:
Hey! This is a tough one — what you are seeing there is how we support classes like this:

```html
<div class="content-[']']">
```

https://play.tailwindcss.com/8zudTG6Nal

Not sure what the right trade off to make here in ambiguous situations, especially given the constraint that this has to be achievable with a regex to be as universal and simple as possible.

Any suggestions on heuristics we can use to determine when yo keep the quote/square bracket and when not to? 🤔 

Oh cool, that makes a lot of sense. I didn't realize `'` was even valid as part of a class name.

Are quote characters inside the brackets valid/useful for any utilities _other_ than `content-`? My only suggestion really would be if `content-` is the only thing that needs it, stop at the first matching bracket _except_ right after a `content-`, so that `content-[']']` would work but something like `w-[']']` would just match `w-[']`. Adding that to an existing regex would likely be messy though, and adding an additional separate regex probably isn't a trade off that makes sense (if that's even possible).

What are the regexes the JIT engine uses to match class names with dynamic values like this? I looked around and found a couple but none seemed to actually work against `content-[']']`, they stopped at the quotes. I'm curious to see what they're like in case I think of something else.

Adding a trailing space for now feels like a more than fair workaround. I'll keep thinking about it but feel free to close this if it's not a priority 👍🏻 thanks!

Patch:
diff --git a/src/jit/lib/expandTailwindAtRules.js b/src/jit/lib/expandTailwindAtRules.js
--- 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 = {


Test patch:
diff --git a/tests/jit/extractor-edge-cases.test.js b/tests/jit/extractor-edge-cases.test.js
new file mode 100644
--- /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]';
+      }
+    `)
+  })
+})