Repository: tailwindlabs/tailwindcss (TypeScript)

Issue:
double escape happening in attribute value selectors
<!-- Please provide all of the information requested below. We're a small team and without all of this information it's not possible for us to help and your bug report will be closed. -->

**What version of Tailwind CSS are you using?**

v3.2.4

**What build tool (or framework if it abstracts the build tool) are you using?**

postcss 8.4.19

**What version of Node.js are you using?**

v14.19.1

**What browser are you using?**

Chrome

**What operating system are you using?**

Kubuntu Linux

**Reproduction URL**

https://play.tailwindcss.com/sRVqoiJfLV

**Describe your issue**

in the attribute value of a attribute selector the value is being double escaped and there appears to be no way to un-escape it.


Hints:

Patch:
diff --git a/src/corePlugins.js b/src/corePlugins.js
--- a/src/corePlugins.js
+++ b/src/corePlugins.js
@@ -168,7 +168,30 @@ export let variantPlugins = {
           if (!result.includes('&')) result = '&' + result
 
           let [a, b] = fn('', extra)
-          return result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b)
+
+          let start = null
+          let end = null
+          let quotes = 0
+
+          for (let i = 0; i < result.length; ++i) {
+            let c = result[i]
+            if (c === '&') {
+              start = i
+            } else if (c === "'" || c === '"') {
+              quotes += 1
+            } else if (start !== null && c === ' ' && !quotes) {
+              end = i
+            }
+          }
+
+          if (start !== null && end === null) {
+            end = result.length
+          }
+
+          // Basically this but can handle quotes:
+          // result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b)
+
+          return result.slice(0, start) + a + result.slice(start + 1, end) + b + result.slice(end)
         },
         { values: Object.fromEntries(pseudoVariants) }
       )
diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js
--- a/src/lib/generateRules.js
+++ b/src/lib/generateRules.js
@@ -152,10 +152,18 @@ function applyVariant(variant, matches, context) {
 
   // Retrieve "modifier"
   {
-    let match = /(.*)\/(.*)$/g.exec(variant)
-    if (match && !context.variantMap.has(variant)) {
-      variant = match[1]
-      args.modifier = match[2]
+    let [baseVariant, ...modifiers] = splitAtTopLevelOnly(variant, '/')
+
+    // This is a hack to support variants with `/` in them, like `ar-1/10/20:text-red-500`
+    // In this case 1/10 is a value but /20 is a modifier
+    if (modifiers.length > 1) {
+      baseVariant = baseVariant + '/' + modifiers.slice(0, -1).join('/')
+      modifiers = modifiers.slice(-1)
+    }
+
+    if (modifiers.length && !context.variantMap.has(variant)) {
+      variant = baseVariant
+      args.modifier = modifiers[0]
 
       if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
         return []


Test patch:
diff --git a/tests/basic-usage.test.js b/tests/basic-usage.test.js
--- a/tests/basic-usage.test.js
+++ b/tests/basic-usage.test.js
@@ -949,4 +949,66 @@ crosscheck(({ stable, oxide }) => {
       `)
     })
   })
+
+  test('detects quoted arbitrary values containing a slash', async () => {
+    let config = {
+      content: [
+        {
+          raw: html`<div class="group-[[href^='/']]:hidden"></div>`,
+        },
+      ],
+    }
+
+    let input = css`
+      @tailwind utilities;
+    `
+
+    let result = await run(input, config)
+
+    oxide.expect(result.css).toMatchFormattedCss(css`
+      .group[href^='/'] .group-\[\[href\^\=\'\/\'\]\]\:hidden {
+        display: none;
+      }
+    `)
+
+    stable.expect(result.css).toMatchFormattedCss(css`
+      .hidden {
+        display: none;
+      }
+      .group[href^='/'] .group-\[\[href\^\=\'\/\'\]\]\:hidden {
+        display: none;
+      }
+    `)
+  })
+
+  test('handled quoted arbitrary values containing escaped spaces', async () => {
+    let config = {
+      content: [
+        {
+          raw: html`<div class="group-[[href^='_bar']]:hidden"></div>`,
+        },
+      ],
+    }
+
+    let input = css`
+      @tailwind utilities;
+    `
+
+    let result = await run(input, config)
+
+    oxide.expect(result.css).toMatchFormattedCss(css`
+      .group[href^=' bar'] .group-\[\[href\^\=\'_bar\'\]\]\:hidden {
+        display: none;
+      }
+    `)
+
+    stable.expect(result.css).toMatchFormattedCss(css`
+      .hidden {
+        display: none;
+      }
+      .group[href^=' bar'] .group-\[\[href\^\=\'_bar\'\]\]\:hidden {
+        display: none;
+      }
+    `)
+  })
 })
diff --git a/tests/util/run.js b/tests/util/run.js
--- a/tests/util/run.js
+++ b/tests/util/run.js
@@ -60,6 +60,18 @@ let nullProxy = new Proxy(
   }
 )
 
+/**
+ * @typedef {object} CrossCheck
+ * @property {typeof import('@jest/globals')} oxide
+ * @property {typeof import('@jest/globals')} stable
+ * @property {object} engine
+ * @property {boolean} engine.oxide
+ * @property {boolean} engine.stable
+ */
+
+/**
+ * @param {(data: CrossCheck) => void} fn
+ */
 export function crosscheck(fn) {
   let engines =
     env.ENGINE === 'oxide' ? [{ engine: 'Stable' }, { engine: 'Oxide' }] : [{ engine: 'Stable' }]