Skip to content

Commit

Permalink
Properly extract classes with arbitrary values in arrays and classes …
Browse files Browse the repository at this point in the history
…followed by escaped quotes (#6590)

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
  • Loading branch information
thecrypticace and RobinMalfait committed Dec 17, 2021
1 parent 2fdbe10 commit 27c67fe
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Don't mutate custom color palette when overriding per-plugin colors ([#6546](https://github.com/tailwindlabs/tailwindcss/pull/6546))
- Improve circular dependency detection when using `@apply` ([#6588](https://github.com/tailwindlabs/tailwindcss/pull/6588))
- Only generate variants for non-`user` layers ([#6589](https://github.com/tailwindlabs/tailwindcss/pull/6589))
- Properly extract classes with arbitrary values in arrays and classes followed by escaped quotes ([#6590](https://github.com/tailwindlabs/tailwindcss/pull/6590))

## [3.0.6] - 2021-12-16

Expand Down
31 changes: 31 additions & 0 deletions src/lib/defaultExtractor.js
@@ -0,0 +1,31 @@
const PATTERNS = [
/(?:\['([^'\s]+[^<>"'`\s:\\])')/.source, // ['text-lg' -> text-lg
/(?:\["([^"\s]+[^<>"'`\s:\\])")/.source, // ["text-lg" -> text-lg
/(?:\[`([^`\s]+[^<>"'`\s:\\])`)/.source, // [`text-lg` -> text-lg
/([^<>"'`\s]*\[\w*'[^"`\s]*'?\])/.source, // font-['some_font',sans-serif]
/([^<>"'`\s]*\[\w*"[^'`\s]*"?\])/.source, // font-["some_font",sans-serif]
/([^<>"'`\s]*\[\w*\('[^"'`\s]*'\)\])/.source, // bg-[url('...')]
/([^<>"'`\s]*\[\w*\("[^"'`\s]*"\)\])/.source, // bg-[url("...")]
/([^<>"'`\s]*\[\w*\('[^"`\s]*'\)\])/.source, // bg-[url('...'),url('...')]
/([^<>"'`\s]*\[\w*\("[^'`\s]*"\)\])/.source, // bg-[url("..."),url("...")]
/([^<>"'`\s]*\['[^"'`\s]*'\])/.source, // `content-['hello']` but not `content-['hello']']`
/([^<>"'`\s]*\["[^"'`\s]*"\])/.source, // `content-["hello"]` but not `content-["hello"]"]`
/([^<>"'`\s]*\[[^<>"'`\s]*:'[^"'`\s]*'\])/.source, // `[content:'hello']` but not `[content:"hello"]`
/([^<>"'`\s]*\[[^<>"'`\s]*:"[^"'`\s]*"\])/.source, // `[content:"hello"]` but not `[content:'hello']`
/([^<>"'`\s]*\[[^"'`\s]+\][^<>"'`\s]*)/.source, // `fill-[#bada55]`, `fill-[#bada55]/50`
/([^<>"'`\s]*[^"'`\s:\\])/.source, // `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

/**
* @param {string} content
*/
export function defaultExtractor(content) {
let broadMatches = content.matchAll(BROAD_MATCH_GLOBAL_REGEXP)
let innerMatches = content.match(INNER_MATCH_GLOBAL_REGEXP) || []
let results = [...broadMatches, ...innerMatches].flat().filter((v) => v !== undefined)

return results
}
25 changes: 2 additions & 23 deletions src/lib/expandTailwindAtRules.js
Expand Up @@ -3,33 +3,12 @@ import * as sharedState from './sharedState'
import { generateRules } from './generateRules'
import bigSign from '../util/bigSign'
import cloneNodes from '../util/cloneNodes'
import { defaultExtractor } from './defaultExtractor'

let env = sharedState.env

const PATTERNS = [
/([^<>"'`\s]*\[\w*'[^"`\s]*'?\])/.source, // font-['some_font',sans-serif]
/([^<>"'`\s]*\[\w*"[^"`\s]*"?\])/.source, // font-["some_font",sans-serif]
/([^<>"'`\s]*\[\w*\('[^"'`\s]*'\)\])/.source, // bg-[url('...')]
/([^<>"'`\s]*\[\w*\("[^"'`\s]*"\)\])/.source, // bg-[url("...")]
/([^<>"'`\s]*\[\w*\('[^"`\s]*'\)\])/.source, // bg-[url('...'),url('...')]
/([^<>"'`\s]*\[\w*\("[^'`\s]*"\)\])/.source, // bg-[url("..."),url("...")]
/([^<>"'`\s]*\['[^"'`\s]*'\])/.source, // `content-['hello']` but not `content-['hello']']`
/([^<>"'`\s]*\["[^"'`\s]*"\])/.source, // `content-["hello"]` but not `content-["hello"]"]`
/([^<>"'`\s]*\[[^<>"'`\s]*:'[^"'`\s]*'\])/.source, // `[content:'hello']` but not `[content:"hello"]`
/([^<>"'`\s]*\[[^<>"'`\s]*:"[^"'`\s]*"\])/.source, // `[content:"hello"]` but not `[content:'hello']`
/([^<>"'`\s]*\[[^"'`\s]+\][^<>"'`\s]*)/.source, // `fill-[#bada55]`, `fill-[#bada55]/50`
/([^<>"'`\s]*[^"'`\s:])/.source, // `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 = {
DEFAULT: (content) => {
let broadMatches = content.match(BROAD_MATCH_GLOBAL_REGEXP) || []
let innerMatches = content.match(INNER_MATCH_GLOBAL_REGEXP) || []

return [...broadMatches, ...innerMatches]
},
DEFAULT: defaultExtractor,
}

const builtInTransformers = {
Expand Down
108 changes: 108 additions & 0 deletions tests/default-extractor.test.js
@@ -0,0 +1,108 @@
import { html } from './util/run'
import { defaultExtractor } from '../src/lib/defaultExtractor'

const input = html`
<div class="font-['some_font',sans-serif]"></div>
<div class='font-["some_font",sans-serif]'></div>
<div class="bg-[url('...')]"></div>
<div class="bg-[url("...")]"></div>
<div class="bg-[url('...'),url('...')]"></div>
<div class="bg-[url("..."),url("...")]"></div>
<div class="content-['hello']"></div>
<div class="content-['hello']']"></div>
<div class="content-["hello"]"></div>
<div class="content-["hello"]"]"></div>
<div class="[content:'hello']"></div>
<div class="[content:"hello"]"></div>
<div class="[content:"hello"]"></div>
<div class="[content:'hello']"></div>
<div class="fill-[#bada55]"></div>
<div class="fill-[#bada55]/50"></div>
<div class="px-1.5"></div>
<div class="uppercase"></div>
<div class="uppercase:"></div>
<div class="hover:font-bold"></div>
<div class="content-['>']"></div>
<script>
let classes01 = ["text-[10px]"]
let classes02 = ["hover:font-bold"]
let classes03 = {"code": "<div class=\"text-sm text-blue-500\"></div>"}
let classes04 = ['text-[11px]']
let classes05 = ['text-[21px]', 'text-[22px]', 'lg:text-[24px]']
let classes06 = ["text-[31px]", "text-[32px]"]
let classes07 = [${'`'}text-[41px]${'`'}, ${'`'}text-[42px]${'`'}]
let classes08 = {"text-[51px]":"text-[52px]"}
let classes09 = {'text-[61px]':'text-[62px]'}
let classes10 = {${'`'}text-[71px]${'`'}:${'`'}text-[72px]${'`'}}
let classes11 = ['hover:']
let classes12 = ['hover:\'abc']
let classes13 = ["lg:text-[4px]"]
let classes14 = ["<div class='hover:test'>"]
let obj = {
uppercase:true
}
</script>
`

const includes = [
`font-['some_font',sans-serif]`,
`font-["some_font",sans-serif]`,
`bg-[url('...')]`,
`bg-[url("...")]`,
`bg-[url('...'),url('...')]`,
`bg-[url("..."),url("...")]`,
`content-['hello']`,
`content-["hello"]`,
`[content:'hello']`,
`[content:"hello"]`,
`[content:"hello"]`,
`[content:'hello']`,
`fill-[#bada55]`,
`fill-[#bada55]/50`,
`px-1.5`,
`uppercase`,
`hover:font-bold`,
`text-sm`,
`text-[10px]`,
`text-[11px]`,
`text-blue-500`,
`text-[21px]`,
`text-[22px]`,
`text-[31px]`,
`text-[32px]`,
`text-[41px]`,
`text-[42px]`,
`text-[51px]`,
`text-[52px]`,
`text-[61px]`,
`text-[62px]`,
`text-[71px]`,
`text-[72px]`,
`lg:text-[4px]`,
`lg:text-[24px]`,
`content-['>']`,
`hover:test`,
]

const excludes = [
`uppercase:`,
'hover:',
"hover:'abc",
`font-bold`,
`<div class='hover:test'>`,
`test`,
]

test('The default extractor works as expected', async () => {
const extractions = defaultExtractor(input.trim())

for (const str of includes) {
expect(extractions).toContain(str)
}

for (const str of excludes) {
expect(extractions).not.toContain(str)
}
})

0 comments on commit 27c67fe

Please sign in to comment.