diff --git a/CHANGELOG.md b/CHANGELOG.md index b051bbf9dc9c..efaf131d9ba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/lib/defaultExtractor.js b/src/lib/defaultExtractor.js new file mode 100644 index 000000000000..32c18fe6bca2 --- /dev/null +++ b/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 +} diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index 087a671ea17a..addf2c666169 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -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 = { diff --git a/tests/default-extractor.test.js b/tests/default-extractor.test.js new file mode 100644 index 000000000000..08315ca49a89 --- /dev/null +++ b/tests/default-extractor.test.js @@ -0,0 +1,108 @@ +import { html } from './util/run' +import { defaultExtractor } from '../src/lib/defaultExtractor' + +const input = html` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +` + +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`, + `
`, + `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) + } +})