diff --git a/CHANGELOG.md b/CHANGELOG.md index 6156ddff6100..cd5c74bffdeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Nothing yet! +## [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)) + ## [3.4.6] - 2024-07-16 ### Fixed @@ -2405,7 +2412,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 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 : '') ) } 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", 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/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/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', () => {