Skip to content

Commit 27c67fe

Browse files
Properly extract classes with arbitrary values in arrays and classes followed by escaped quotes (#6590)
Co-authored-by: Robin Malfait <[email protected]>
1 parent 2fdbe10 commit 27c67fe

File tree

4 files changed

+142
-23
lines changed

4 files changed

+142
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Don't mutate custom color palette when overriding per-plugin colors ([#6546](https://github.com/tailwindlabs/tailwindcss/pull/6546))
1313
- Improve circular dependency detection when using `@apply` ([#6588](https://github.com/tailwindlabs/tailwindcss/pull/6588))
1414
- Only generate variants for non-`user` layers ([#6589](https://github.com/tailwindlabs/tailwindcss/pull/6589))
15+
- Properly extract classes with arbitrary values in arrays and classes followed by escaped quotes ([#6590](https://github.com/tailwindlabs/tailwindcss/pull/6590))
1516

1617
## [3.0.6] - 2021-12-16
1718

src/lib/defaultExtractor.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const PATTERNS = [
2+
/(?:\['([^'\s]+[^<>"'`\s:\\])')/.source, // ['text-lg' -> text-lg
3+
/(?:\["([^"\s]+[^<>"'`\s:\\])")/.source, // ["text-lg" -> text-lg
4+
/(?:\[`([^`\s]+[^<>"'`\s:\\])`)/.source, // [`text-lg` -> text-lg
5+
/([^<>"'`\s]*\[\w*'[^"`\s]*'?\])/.source, // font-['some_font',sans-serif]
6+
/([^<>"'`\s]*\[\w*"[^'`\s]*"?\])/.source, // font-["some_font",sans-serif]
7+
/([^<>"'`\s]*\[\w*\('[^"'`\s]*'\)\])/.source, // bg-[url('...')]
8+
/([^<>"'`\s]*\[\w*\("[^"'`\s]*"\)\])/.source, // bg-[url("...")]
9+
/([^<>"'`\s]*\[\w*\('[^"`\s]*'\)\])/.source, // bg-[url('...'),url('...')]
10+
/([^<>"'`\s]*\[\w*\("[^'`\s]*"\)\])/.source, // bg-[url("..."),url("...")]
11+
/([^<>"'`\s]*\['[^"'`\s]*'\])/.source, // `content-['hello']` but not `content-['hello']']`
12+
/([^<>"'`\s]*\["[^"'`\s]*"\])/.source, // `content-["hello"]` but not `content-["hello"]"]`
13+
/([^<>"'`\s]*\[[^<>"'`\s]*:'[^"'`\s]*'\])/.source, // `[content:'hello']` but not `[content:"hello"]`
14+
/([^<>"'`\s]*\[[^<>"'`\s]*:"[^"'`\s]*"\])/.source, // `[content:"hello"]` but not `[content:'hello']`
15+
/([^<>"'`\s]*\[[^"'`\s]+\][^<>"'`\s]*)/.source, // `fill-[#bada55]`, `fill-[#bada55]/50`
16+
/([^<>"'`\s]*[^"'`\s:\\])/.source, // `px-1.5`, `uppercase` but not `uppercase:`
17+
].join('|')
18+
19+
const BROAD_MATCH_GLOBAL_REGEXP = new RegExp(PATTERNS, 'g')
20+
const INNER_MATCH_GLOBAL_REGEXP = /[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g
21+
22+
/**
23+
* @param {string} content
24+
*/
25+
export function defaultExtractor(content) {
26+
let broadMatches = content.matchAll(BROAD_MATCH_GLOBAL_REGEXP)
27+
let innerMatches = content.match(INNER_MATCH_GLOBAL_REGEXP) || []
28+
let results = [...broadMatches, ...innerMatches].flat().filter((v) => v !== undefined)
29+
30+
return results
31+
}

src/lib/expandTailwindAtRules.js

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,12 @@ import * as sharedState from './sharedState'
33
import { generateRules } from './generateRules'
44
import bigSign from '../util/bigSign'
55
import cloneNodes from '../util/cloneNodes'
6+
import { defaultExtractor } from './defaultExtractor'
67

78
let env = sharedState.env
89

9-
const PATTERNS = [
10-
/([^<>"'`\s]*\[\w*'[^"`\s]*'?\])/.source, // font-['some_font',sans-serif]
11-
/([^<>"'`\s]*\[\w*"[^"`\s]*"?\])/.source, // font-["some_font",sans-serif]
12-
/([^<>"'`\s]*\[\w*\('[^"'`\s]*'\)\])/.source, // bg-[url('...')]
13-
/([^<>"'`\s]*\[\w*\("[^"'`\s]*"\)\])/.source, // bg-[url("...")]
14-
/([^<>"'`\s]*\[\w*\('[^"`\s]*'\)\])/.source, // bg-[url('...'),url('...')]
15-
/([^<>"'`\s]*\[\w*\("[^'`\s]*"\)\])/.source, // bg-[url("..."),url("...")]
16-
/([^<>"'`\s]*\['[^"'`\s]*'\])/.source, // `content-['hello']` but not `content-['hello']']`
17-
/([^<>"'`\s]*\["[^"'`\s]*"\])/.source, // `content-["hello"]` but not `content-["hello"]"]`
18-
/([^<>"'`\s]*\[[^<>"'`\s]*:'[^"'`\s]*'\])/.source, // `[content:'hello']` but not `[content:"hello"]`
19-
/([^<>"'`\s]*\[[^<>"'`\s]*:"[^"'`\s]*"\])/.source, // `[content:"hello"]` but not `[content:'hello']`
20-
/([^<>"'`\s]*\[[^"'`\s]+\][^<>"'`\s]*)/.source, // `fill-[#bada55]`, `fill-[#bada55]/50`
21-
/([^<>"'`\s]*[^"'`\s:])/.source, // `px-1.5`, `uppercase` but not `uppercase:`
22-
].join('|')
23-
const BROAD_MATCH_GLOBAL_REGEXP = new RegExp(PATTERNS, 'g')
24-
const INNER_MATCH_GLOBAL_REGEXP = /[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g
25-
2610
const builtInExtractors = {
27-
DEFAULT: (content) => {
28-
let broadMatches = content.match(BROAD_MATCH_GLOBAL_REGEXP) || []
29-
let innerMatches = content.match(INNER_MATCH_GLOBAL_REGEXP) || []
30-
31-
return [...broadMatches, ...innerMatches]
32-
},
11+
DEFAULT: defaultExtractor,
3312
}
3413

3514
const builtInTransformers = {

tests/default-extractor.test.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { html } from './util/run'
2+
import { defaultExtractor } from '../src/lib/defaultExtractor'
3+
4+
const input = html`
5+
<div class="font-['some_font',sans-serif]"></div>
6+
<div class='font-["some_font",sans-serif]'></div>
7+
<div class="bg-[url('...')]"></div>
8+
<div class="bg-[url("...")]"></div>
9+
<div class="bg-[url('...'),url('...')]"></div>
10+
<div class="bg-[url("..."),url("...")]"></div>
11+
<div class="content-['hello']"></div>
12+
<div class="content-['hello']']"></div>
13+
<div class="content-["hello"]"></div>
14+
<div class="content-["hello"]"]"></div>
15+
<div class="[content:'hello']"></div>
16+
<div class="[content:"hello"]"></div>
17+
<div class="[content:"hello"]"></div>
18+
<div class="[content:'hello']"></div>
19+
<div class="fill-[#bada55]"></div>
20+
<div class="fill-[#bada55]/50"></div>
21+
<div class="px-1.5"></div>
22+
<div class="uppercase"></div>
23+
<div class="uppercase:"></div>
24+
<div class="hover:font-bold"></div>
25+
<div class="content-['>']"></div>
26+
27+
<script>
28+
let classes01 = ["text-[10px]"]
29+
let classes02 = ["hover:font-bold"]
30+
let classes03 = {"code": "<div class=\"text-sm text-blue-500\"></div>"}
31+
let classes04 = ['text-[11px]']
32+
let classes05 = ['text-[21px]', 'text-[22px]', 'lg:text-[24px]']
33+
let classes06 = ["text-[31px]", "text-[32px]"]
34+
let classes07 = [${'`'}text-[41px]${'`'}, ${'`'}text-[42px]${'`'}]
35+
let classes08 = {"text-[51px]":"text-[52px]"}
36+
let classes09 = {'text-[61px]':'text-[62px]'}
37+
let classes10 = {${'`'}text-[71px]${'`'}:${'`'}text-[72px]${'`'}}
38+
let classes11 = ['hover:']
39+
let classes12 = ['hover:\'abc']
40+
let classes13 = ["lg:text-[4px]"]
41+
let classes14 = ["<div class='hover:test'>"]
42+
43+
let obj = {
44+
uppercase:true
45+
}
46+
</script>
47+
`
48+
49+
const includes = [
50+
`font-['some_font',sans-serif]`,
51+
`font-["some_font",sans-serif]`,
52+
`bg-[url('...')]`,
53+
`bg-[url("...")]`,
54+
`bg-[url('...'),url('...')]`,
55+
`bg-[url("..."),url("...")]`,
56+
`content-['hello']`,
57+
`content-["hello"]`,
58+
`[content:'hello']`,
59+
`[content:"hello"]`,
60+
`[content:"hello"]`,
61+
`[content:'hello']`,
62+
`fill-[#bada55]`,
63+
`fill-[#bada55]/50`,
64+
`px-1.5`,
65+
`uppercase`,
66+
`hover:font-bold`,
67+
`text-sm`,
68+
`text-[10px]`,
69+
`text-[11px]`,
70+
`text-blue-500`,
71+
`text-[21px]`,
72+
`text-[22px]`,
73+
`text-[31px]`,
74+
`text-[32px]`,
75+
`text-[41px]`,
76+
`text-[42px]`,
77+
`text-[51px]`,
78+
`text-[52px]`,
79+
`text-[61px]`,
80+
`text-[62px]`,
81+
`text-[71px]`,
82+
`text-[72px]`,
83+
`lg:text-[4px]`,
84+
`lg:text-[24px]`,
85+
`content-['>']`,
86+
`hover:test`,
87+
]
88+
89+
const excludes = [
90+
`uppercase:`,
91+
'hover:',
92+
"hover:'abc",
93+
`font-bold`,
94+
`<div class='hover:test'>`,
95+
`test`,
96+
]
97+
98+
test('The default extractor works as expected', async () => {
99+
const extractions = defaultExtractor(input.trim())
100+
101+
for (const str of includes) {
102+
expect(extractions).toContain(str)
103+
}
104+
105+
for (const str of excludes) {
106+
expect(extractions).not.toContain(str)
107+
}
108+
})

0 commit comments

Comments
 (0)