From bdc87ae1d78b4989bddd30176e0bfdda6df775cd Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 18 Jul 2024 11:21:20 -0400 Subject: [PATCH 1/7] Fix class detection in Slim templates with attached attributes and IDs (#14019) * Fix class detection in Slim templates with attached attributes and IDs * Update changelog * Tweak regex --- CHANGELOG.md | 2 +- src/lib/defaultExtractor.js | 3 +++ tests/default-extractor.test.js | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6156ddff6100..0be64b34e3f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Nothing yet! +- Fix class detection in Slim templates with attached attributes and ID ([#14019](https://github.com/tailwindlabs/tailwindcss/pull/14019)) ## [3.4.6] - 2024-07-16 diff --git a/src/lib/defaultExtractor.js b/src/lib/defaultExtractor.js index 8f89596313ce..34407794fec9 100644 --- a/src/lib/defaultExtractor.js +++ b/src/lib/defaultExtractor.js @@ -152,6 +152,9 @@ function* buildRegExps(context) { utility, ]) } + + // 5. Inner matches + yield /[^<>"'`\s.(){}[\]#=%$][^<>"'`\s(){}[\]#=%$]*[^<>"'`\s.(){}[\]#=%:$]/g } // We want to capture any "special" characters diff --git a/tests/default-extractor.test.js b/tests/default-extractor.test.js index 0cac75d0c299..a0b6384efd25 100644 --- a/tests/default-extractor.test.js +++ b/tests/default-extractor.test.js @@ -486,6 +486,26 @@ test('classes in slim templates starting with number', async () => { expect(extractions).toContain('2xl:bg-red-300') }) +test('classes in slim templates with attributes added', () => { + let extractions = defaultExtractor(` + .ml-auto[ + data-value='foo' + ] + Foo bar + .mr-auto[data-value='foo'] + Foo bar + .mt-auto#omg + Foo bar + #omg.mb-auto + Foo bar + `) + + expect(extractions).toContain(`ml-auto`) + expect(extractions).toContain(`mr-auto`) + expect(extractions).toContain(`mt-auto`) + expect(extractions).toContain(`mb-auto`) +}) + test("classes with fractional numeric values don't also generate the whole number utility", async () => { const extractions = defaultExtractor(`
Hello world!
From 866860e6a687c0f9f24cc8b736d5ce7334ec2d41 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 23 Jul 2024 14:40:11 +0200 Subject: [PATCH 2/7] Print eventual lightning CSS parsing errors when the CSS matcher fail (#14034) --- jest/customMatchers.js | 73 ++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/jest/customMatchers.js b/jest/customMatchers.js index 9b1c6b6d060c..c41696ce8526 100644 --- a/jest/customMatchers.js +++ b/jest/customMatchers.js @@ -23,39 +23,42 @@ function formatPrettier(input) { function format(input) { try { - return lightningcss - .transform({ - filename: 'input.css', - code: Buffer.from(input), - minify: false, - targets: { chrome: 106 << 16 }, - drafts: { - nesting: true, - customMedia: true, - }, - }) - .code.toString('utf8') + return [ + lightningcss + .transform({ + filename: 'input.css', + code: Buffer.from(input), + minify: false, + targets: { chrome: 106 << 16 }, + drafts: { + nesting: true, + customMedia: true, + }, + }) + .code.toString('utf8'), + null, + ] } catch (err) { + let lines = err.source.split('\n') + let e = new Error( + [ + 'Error formatting using Lightning CSS:', + '', + ...[ + '```css', + ...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line), + ' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(), + ...lines.slice(err.loc.line, err.loc.line + 2), + '```', + ], + ].join('\n') + ) try { // Lightning CSS is pretty strict, so it will fail for `@media screen(md) {}` for example, // in that case we can fallback to prettier since it doesn't really care. However if an // actual syntax error is made, then we still want to show the proper error. - return formatPrettier(input.replace(/\n/g, '')) + return [formatPrettier(input.replace(/\n/g, '')), e] } catch { - let lines = err.source.split('\n') - let e = new Error( - [ - 'Error formatting using Lightning CSS:', - '', - ...[ - '```css', - ...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line), - ' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(), - ...lines.slice(err.loc.line, err.loc.line + 2), - '```', - ], - ].join('\n') - ) if (Error.captureStackTrace) { Error.captureStackTrace(e, toMatchFormattedCss) } @@ -71,8 +74,8 @@ function toMatchFormattedCss(received = '', argument = '') { promise: this.promise, } - let formattedReceived = format(received) - let formattedArgument = format(argument) + let [formattedReceived, formattingReceivedError] = format(received) + let [formattedArgument, formattingArgumentError] = format(argument) let pass = formattedReceived === formattedArgument @@ -99,7 +102,9 @@ function toMatchFormattedCss(received = '', argument = '') { (diffString && diffString.includes('- Expect') ? `Difference:\n\n${diffString}` : `Expected: ${this.utils.printExpected(expected)}\n` + - `Received: ${this.utils.printReceived(actual)}`) + `Received: ${this.utils.printReceived(actual)}`) + + (formattingReceivedError ? '\n\n' + formattingReceivedError : '') + + (formattingArgumentError ? '\n\n' + formattingArgumentError : '') ) } @@ -118,7 +123,9 @@ expect.extend({ promise: this.promise, } - let pass = format(received).includes(format(argument)) + let [formattedReceived, formattedReceivedError] = format(received) + let [formattedArgument, formattedArgumentError] = format(argument) + let pass = formattedReceived.includes(formattedArgument) let message = pass ? () => { @@ -143,7 +150,9 @@ expect.extend({ (diffString && diffString.includes('- Expect') ? `Difference:\n\n${diffString}` : `Expected: ${this.utils.printExpected(expected)}\n` + - `Received: ${this.utils.printReceived(actual)}`) + `Received: ${this.utils.printReceived(actual)}`) + + (formattedReceivedError ? '\n\n' + formattedReceivedError : '') + + (formattedArgumentError ? '\n\n' + formattedArgumentError : '') ) } From 680c55c11cd0960da7efd57d0c2f90b821be8946 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 24 Jul 2024 18:46:21 +0200 Subject: [PATCH 3/7] Normalize attribute selector for `data-*` and `aria-*` modifiers (#14037) Fixes #14026 See #14040 for the v4 fix When translating `data-` and `aria-` modifiers with attribute selectors, we currently do not wrap the target attribute in quotes. This only works for keywords (purely alphabetic words) but breaks as soon as there are numbers or things like spaces in the attribute: ```html
underlined
not underlined
not underlined
``` Since it's fairly common to have attribute selectors with `data-` and `aria-` modifiers, this PR will now wrap the attribute in quotes unless these are already wrapped. | Tailwind Modifier | CSS Selector | | ------------- | ------------- | | `.data-[id=foo]` | `[data-id='foo']` | | `.data-[id='foo']` | `[data-id='foo']` | | `.data-[id=foo_i]` | `[data-id='foo i']` | | `.data-[id='foo'_i]` | `[data-id='foo' i]` | | `.data-[id=123]` | `[data-id='123']` | --------- Co-authored-by: Robin Malfait --- CHANGELOG.md | 1 + src/corePlugins.js | 26 ++++++++------ src/util/dataTypes.js | 28 +++++++++++++++ tests/arbitrary-variants.test.js | 58 ++++++++++++++++++++++++++++++-- 4 files changed, 100 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0be64b34e3f0..b350588acd37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Fix class detection in Slim templates with attached attributes and ID ([#14019](https://github.com/tailwindlabs/tailwindcss/pull/14019)) +- Attribute selectors in `data-*` and `aria-*` modifiers are now wrapped in quotation marks by default, allowing numbers and spaces in them ([#14037])(https://github.com/tailwindlabs/tailwindcss/pull/14037) ## [3.4.6] - 2024-07-16 diff --git a/src/corePlugins.js b/src/corePlugins.js index f3441f4b88a3..8f274153879e 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -21,7 +21,7 @@ import { import { formatBoxShadowValue, parseBoxShadowValue } from './util/parseBoxShadowValue' import { removeAlphaVariables } from './util/removeAlphaVariables' import { flagEnabled } from './featureFlags' -import { normalize } from './util/dataTypes' +import { normalize, normalizeAttributeSelectors } from './util/dataTypes' import { INTERNAL_FEATURES } from './lib/setupContextUtils' export let variantPlugins = { @@ -472,41 +472,45 @@ export let variantPlugins = { }, ariaVariants: ({ matchVariant, theme }) => { - matchVariant('aria', (value) => `&[aria-${normalize(value)}]`, { values: theme('aria') ?? {} }) + matchVariant('aria', (value) => `&[aria-${normalizeAttributeSelectors(normalize(value))}]`, { + values: theme('aria') ?? {}, + }) matchVariant( 'group-aria', (value, { modifier }) => modifier - ? `:merge(.group\\/${modifier})[aria-${normalize(value)}] &` - : `:merge(.group)[aria-${normalize(value)}] &`, + ? `:merge(.group\\/${modifier})[aria-${normalizeAttributeSelectors(normalize(value))}] &` + : `:merge(.group)[aria-${normalizeAttributeSelectors(normalize(value))}] &`, { values: theme('aria') ?? {} } ) matchVariant( 'peer-aria', (value, { modifier }) => modifier - ? `:merge(.peer\\/${modifier})[aria-${normalize(value)}] ~ &` - : `:merge(.peer)[aria-${normalize(value)}] ~ &`, + ? `:merge(.peer\\/${modifier})[aria-${normalizeAttributeSelectors(normalize(value))}] ~ &` + : `:merge(.peer)[aria-${normalizeAttributeSelectors(normalize(value))}] ~ &`, { values: theme('aria') ?? {} } ) }, dataVariants: ({ matchVariant, theme }) => { - matchVariant('data', (value) => `&[data-${normalize(value)}]`, { values: theme('data') ?? {} }) + matchVariant('data', (value) => `&[data-${normalizeAttributeSelectors(normalize(value))}]`, { + values: theme('data') ?? {}, + }) matchVariant( 'group-data', (value, { modifier }) => modifier - ? `:merge(.group\\/${modifier})[data-${normalize(value)}] &` - : `:merge(.group)[data-${normalize(value)}] &`, + ? `:merge(.group\\/${modifier})[data-${normalizeAttributeSelectors(normalize(value))}] &` + : `:merge(.group)[data-${normalizeAttributeSelectors(normalize(value))}] &`, { values: theme('data') ?? {} } ) matchVariant( 'peer-data', (value, { modifier }) => modifier - ? `:merge(.peer\\/${modifier})[data-${normalize(value)}] ~ &` - : `:merge(.peer)[data-${normalize(value)}] ~ &`, + ? `:merge(.peer\\/${modifier})[data-${normalizeAttributeSelectors(normalize(value))}] ~ &` + : `:merge(.peer)[data-${normalizeAttributeSelectors(normalize(value))}] ~ &`, { values: theme('data') ?? {} } ) }, diff --git a/src/util/dataTypes.js b/src/util/dataTypes.js index 85bdbd2059de..e1db13754e45 100644 --- a/src/util/dataTypes.js +++ b/src/util/dataTypes.js @@ -81,6 +81,34 @@ export function normalize(value, context = null, isRoot = true) { return value } +export function normalizeAttributeSelectors(value) { + // Wrap values in attribute selectors with quotes + if (value.includes('=')) { + value = value.replace(/(=.*)/g, (_fullMatch, match) => { + if (match[1] === "'" || match[1] === '"') { + return match + } + + // Handle regex flags on unescaped values + if (match.length > 2) { + let trailingCharacter = match[match.length - 1] + if ( + match[match.length - 2] === ' ' && + (trailingCharacter === 'i' || + trailingCharacter === 'I' || + trailingCharacter === 's' || + trailingCharacter === 'S') + ) { + return `="${match.slice(1, -2)}" ${match[match.length - 1]}` + } + } + + return `="${match.slice(1)}"` + }) + } + return value +} + /** * Add spaces around operators inside math functions * like calc() that do not follow an operator, '(', or `,`. diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js index 0ab9e234c336..c60bb92ad307 100644 --- a/tests/arbitrary-variants.test.js +++ b/tests/arbitrary-variants.test.js @@ -442,6 +442,32 @@ test('keeps escaped underscores with multiple arbitrary variants', () => { }) }) +test('does not add quotes on arbitrary variants', () => { + let config = { + content: [ + { + raw: '
', + }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind base; + @tailwind components; + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + ${defaults} + .\[\&\[data-foo\=\'1\'\]\+\.bar\]\:underline[data-foo='1']+.bar { + text-decoration-line: underline; + } + `) + }) +}) + test('keeps escaped underscores in arbitrary variants mixed with normal variants', () => { let config = { content: [ @@ -601,6 +627,7 @@ it('should support aria variants', () => {
+
@@ -610,6 +637,8 @@ it('should support aria variants', () => {
+
+
@@ -629,16 +658,19 @@ it('should support aria variants', () => { .aria-checked\:underline[aria-checked='true'], .aria-\[labelledby\=\'a_b\'\]\:underline[aria-labelledby='a b'], .aria-\[sort\=ascending\]\:underline[aria-sort='ascending'], + .aria-\[valuenow\=1\]\:underline[aria-valuenow='1'], .group\/foo[aria-checked='true'] .group-aria-checked\/foo\:underline, .group[aria-checked='true'] .group-aria-checked\:underline, .group[aria-labelledby='a b'] .group-aria-\[labelledby\=\'a_b\'\]\:underline, .group\/foo[aria-sort='ascending'] .group-aria-\[sort\=ascending\]\/foo\:underline, .group[aria-sort='ascending'] .group-aria-\[sort\=ascending\]\:underline, + .group[aria-valuenow='1'] .group-aria-\[valuenow\=1\]\:underline, .peer\/foo[aria-checked='true'] ~ .peer-aria-checked\/foo\:underline, .peer[aria-checked='true'] ~ .peer-aria-checked\:underline, .peer[aria-labelledby='a b'] ~ .peer-aria-\[labelledby\=\'a_b\'\]\:underline, .peer\/foo[aria-sort='ascending'] ~ .peer-aria-\[sort\=ascending\]\/foo\:underline, - .peer[aria-sort='ascending'] ~ .peer-aria-\[sort\=ascending\]\:underline { + .peer[aria-sort='ascending'] ~ .peer-aria-\[sort\=ascending\]\:underline, + .peer[aria-valuenow='1'] ~ .peer-aria-\[valuenow\=1\]\:underline { text-decoration-line: underline; } `) @@ -657,8 +689,11 @@ it('should support data variants', () => { raw: html`
-
+
+
+
+
@@ -667,6 +702,12 @@ it('should support data variants', () => {
+
+
+
+
+
+
@@ -685,15 +726,24 @@ it('should support data variants', () => { .underline, .data-checked\:underline[data-ui~='checked'], .data-\[foo\=\'bar_baz\'\]\:underline[data-foo='bar baz'], + .data-\[id\$\=\'foo\'_s\]\:underline[data-id$='foo' s], + .data-\[id\$\=foo_bar_s\]\:underline[data-id$='foo bar' s], + .data-\[id\=0\]\:underline[data-id='0'], .data-\[position\=top\]\:underline[data-position='top'], .group\/foo[data-ui~='checked'] .group-data-checked\/foo\:underline, .group[data-ui~='checked'] .group-data-checked\:underline, .group[data-foo='bar baz'] .group-data-\[foo\=\'bar_baz\'\]\:underline, + .group[data-id$='foo' s] .group-data-\[id\$\=\'foo\'_s\]\:underline, + .group[data-id$='foo bar' s] .group-data-\[id\$\=foo_bar_s\]\:underline, + .group[data-id='0'] .group-data-\[id\=0\]\:underline, .group\/foo[data-position='top'] .group-data-\[position\=top\]\/foo\:underline, .group[data-position='top'] .group-data-\[position\=top\]\:underline, .peer\/foo[data-ui~='checked'] ~ .peer-data-checked\/foo\:underline, .peer[data-ui~='checked'] ~ .peer-data-checked\:underline, .peer[data-foo='bar baz'] ~ .peer-data-\[foo\=\'bar_baz\'\]\:underline, + .peer[data-id$='foo' s] ~ .peer-data-\[id\$\=\'foo\'_s\]\:underline, + .peer[data-id$='foo bar' s] ~ .peer-data-\[id\$\=foo_bar_s\]\:underline, + .peer[data-id='0'] ~ .peer-data-\[id\=0\]\:underline, .peer\/foo[data-position='top'] ~ .peer-data-\[position\=top\]\/foo\:underline, .peer[data-position='top'] ~ .peer-data-\[position\=top\]\:underline { text-decoration-line: underline; @@ -799,6 +849,7 @@ test('has-* variants with arbitrary values', () => {
+
`, }, @@ -836,6 +887,9 @@ test('has-* variants with arbitrary values', () => { .has-\[h2\]\:has-\[\.banana\]\:hidden:has(.banana):has(h2) { display: none; } + .has-\[\[data-foo\=\'1\'\]\+div\]\:font-bold:has([data-foo='1'] + div) { + font-weight: 700; + } `) }) }) From 605d8cd5eb35556206b6b95920b5ff67cf121ec9 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 25 Jul 2024 10:13:56 +0200 Subject: [PATCH 4/7] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b350588acd37..ad6dae25946d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Fix class detection in Slim templates with attached attributes and ID ([#14019](https://github.com/tailwindlabs/tailwindcss/pull/14019)) -- Attribute selectors in `data-*` and `aria-*` modifiers are now wrapped in quotation marks by default, allowing numbers and spaces in them ([#14037])(https://github.com/tailwindlabs/tailwindcss/pull/14037) +- Attribute selectors in `data-*` and `aria-*` modifiers are now wrapped in quotation marks by default, allowing numbers and spaces in them ([#14037](https://github.com/tailwindlabs/tailwindcss/pull/14037)) ## [3.4.6] - 2024-07-16 From 245058c7fd098b1e8a62a4d66615373d8974ddc6 Mon Sep 17 00:00:00 2001 From: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:06:56 -0400 Subject: [PATCH 5/7] Update changelog for v3.4.7 --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6dae25946d..c67058b992c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Nothing yet! + +## [3.4.7] - 2024-07-25 + - Fix class detection in Slim templates with attached attributes and ID ([#14019](https://github.com/tailwindlabs/tailwindcss/pull/14019)) -- Attribute selectors in `data-*` and `aria-*` modifiers are now wrapped in quotation marks by default, allowing numbers and spaces in them ([#14037](https://github.com/tailwindlabs/tailwindcss/pull/14037)) +- Ensure attribute values in `data-*` and `aria-*` modifiers are always quoted in the generated CSS ([#14037](https://github.com/tailwindlabs/tailwindcss/pull/14037)) ## [3.4.6] - 2024-07-16 @@ -2406,7 +2410,8 @@ No release notes - Everything! -[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.6...HEAD +[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.7...HEAD +[3.4.7]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.6...v3.4.7 [3.4.6]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.5...v3.4.6 [3.4.5]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.4...v3.4.5 [3.4.4]: https://github.com/tailwindlabs/tailwindcss/compare/v3.4.3...v3.4.4 From aa6c10f67fff3a62ac93822c1b9ab09690e14bc0 Mon Sep 17 00:00:00 2001 From: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:16:26 -0400 Subject: [PATCH 6/7] Add missing heading to changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c67058b992c7..cd5c74bffdeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [3.4.7] - 2024-07-25 +### Fixed + - Fix class detection in Slim templates with attached attributes and ID ([#14019](https://github.com/tailwindlabs/tailwindcss/pull/14019)) - Ensure attribute values in `data-*` and `aria-*` modifiers are always quoted in the generated CSS ([#14037](https://github.com/tailwindlabs/tailwindcss/pull/14037)) From 9824cb64a03afaa405e184cb31003c8a1efafaa7 Mon Sep 17 00:00:00 2001 From: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:20:52 -0400 Subject: [PATCH 7/7] Update version in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 484886923f44..c09d84f21153 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tailwindcss", - "version": "3.4.6", + "version": "3.4.7", "description": "A utility-first CSS framework for rapidly building custom user interfaces.", "license": "MIT", "main": "lib/index.js",