Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly extract classes with arbitrary values in arrays and classes followed by escaped quotes #6590

Merged
merged 23 commits into from Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
}
})