Properly extract classes with arbitrary values in arrays and classes followed by escaped quotes (#6590)
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This commit is contained in:
parent
2fdbe108cb
commit
27c67fef43
4 changed files with 142 additions and 23 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
31
src/lib/defaultExtractor.js
Normal file
31
src/lib/defaultExtractor.js
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
108
tests/default-extractor.test.js
Normal file
108
tests/default-extractor.test.js
Normal file
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue