diff --git a/CHANGELOG.md b/CHANGELOG.md index 053842c81..e33cbe3af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Don't override explicit `leading-*`, `tracking-*`, or `font-{weight}` utilities with font-size utility defaults ([#14403](https://github.com/tailwindlabs/tailwindcss/pull/14403)) +- Disallow negative bare values in core utilities and variants ([#14453](https://github.com/tailwindlabs/tailwindcss/pull/14453)) ## [4.0.0-alpha.24] - 2024-09-11 diff --git a/packages/tailwindcss/src/compat/default-theme.ts b/packages/tailwindcss/src/compat/default-theme.ts index 5b5c3d2ab..857974214 100644 --- a/packages/tailwindcss/src/compat/default-theme.ts +++ b/packages/tailwindcss/src/compat/default-theme.ts @@ -1,4 +1,5 @@ import type { NamedUtilityValue } from '../candidate' +import { isPositiveInteger } from '../utils/infer-data-type' import { segment } from '../utils/segment' import colors from './colors' import type { UserConfig } from './config/types' @@ -13,31 +14,31 @@ function bareValues(fn: (value: NamedUtilityValue) => string | undefined) { } let bareIntegers = bareValues((value) => { - if (!Number.isNaN(Number(value.value))) { + if (isPositiveInteger(value.value)) { return value.value } }) let barePercentages = bareValues((value: NamedUtilityValue) => { - if (!Number.isNaN(Number(value.value))) { + if (isPositiveInteger(value.value)) { return `${value.value}%` } }) let barePixels = bareValues((value: NamedUtilityValue) => { - if (!Number.isNaN(Number(value.value))) { + if (isPositiveInteger(value.value)) { return `${value.value}px` } }) let bareMilliseconds = bareValues((value: NamedUtilityValue) => { - if (!Number.isNaN(Number(value.value))) { + if (isPositiveInteger(value.value)) { return `${value.value}ms` } }) let bareDegrees = bareValues((value: NamedUtilityValue) => { - if (!Number.isNaN(Number(value.value))) { + if (isPositiveInteger(value.value)) { return `${value.value}deg` } }) @@ -45,12 +46,12 @@ let bareDegrees = bareValues((value: NamedUtilityValue) => { let bareAspectRatio = bareValues((value) => { if (value.fraction === null) return let [lhs, rhs] = segment(value.fraction, '/') - if (!Number.isInteger(Number(lhs)) || !Number.isInteger(Number(rhs))) return + if (!isPositiveInteger(lhs) || !isPositiveInteger(rhs)) return return value.fraction }) let bareRepeatValues = bareValues((value) => { - if (!Number.isNaN(Number(value.value))) { + if (isPositiveInteger(Number(value.value))) { return `repeat(${value.value}, minmax(0, 1fr))` } }) diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 66312ddee..6e22534a7 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -176,6 +176,10 @@ test('inset', async () => { expect( await run([ 'inset', + 'inset--1', + 'inset--1/2', + 'inset--1/-2', + 'inset-1/-2', 'inset-auto/foo', '-inset-full/foo', 'inset-full/foo', @@ -249,6 +253,10 @@ test('inset-x', async () => { expect( await run([ 'inset-x', + 'inset-x--1', + 'inset-x--1/2', + 'inset-x--1/-2', + 'inset-x-1/-2', 'inset-x-auto/foo', 'inset-x-full/foo', '-inset-x-full/foo', @@ -322,6 +330,10 @@ test('inset-y', async () => { expect( await run([ 'inset-y', + 'inset-y--1', + 'inset-y--1/2', + 'inset-y--1/-2', + 'inset-1/-2', 'inset-y-auto/foo', 'inset-y-full/foo', '-inset-y-full/foo', @@ -388,6 +400,10 @@ test('start', async () => { expect( await run([ 'start', + 'start--1', + 'start--1/2', + 'start--1/-2', + 'start-1/-2', 'start-auto/foo', '-start-full/foo', 'start-full/foo', @@ -446,6 +462,10 @@ test('end', async () => { expect( await run([ 'end', + 'end--1', + 'end--1/2', + 'end--1/-2', + 'end-1/-2', 'end-auto/foo', '-end-full/foo', 'end-full/foo', @@ -505,6 +525,10 @@ test('top', async () => { expect( await run([ 'top', + 'top--1', + 'top--1/2', + 'top--1/-2', + 'top-1/-2', 'top-auto/foo', '-top-full/foo', 'top-full/foo', @@ -571,6 +595,10 @@ test('right', async () => { expect( await run([ 'right', + 'right--1', + 'right--1/2', + 'right--1/-2', + 'right-1/-2', 'right-auto/foo', '-right-full/foo', 'right-full/foo', @@ -637,6 +665,10 @@ test('bottom', async () => { expect( await run([ 'bottom', + 'bottom--1', + 'bottom--1/2', + 'bottom--1/-2', + 'bottom-1/-2', 'bottom-auto/foo', '-bottom-full/foo', 'bottom-full/foo', @@ -695,6 +727,10 @@ test('left', async () => { expect( await run([ 'left', + 'left--1', + 'left--1/2', + 'left--1/-2', + 'left-1/-2', 'left-auto/foo', '-left-full/foo', 'left-full/foo', @@ -746,6 +782,7 @@ test('z-index', async () => { expect( await run([ 'z', + 'z--1', '-z-auto', 'z-unknown', 'z-123.5', @@ -801,6 +838,7 @@ test('order', async () => { expect( await run([ 'order', + 'order--4', '-order-first', '-order-last', '-order-none', @@ -856,6 +894,7 @@ test('col', async () => { await run([ 'col', 'col-span', + 'col-span--1', '-col-span-4', 'col-span-unknown', 'col-auto/foo', @@ -895,6 +934,7 @@ test('col-start', async () => { expect( await run([ 'col-start', + 'col-start--1', 'col-start-unknown', 'col-start-auto/foo', 'col-start-4/foo', @@ -931,6 +971,7 @@ test('col-end', async () => { expect( await run([ 'col-end', + 'col-end--1', 'col-end-unknown', 'col-end-auto/foo', 'col-end-4/foo', @@ -980,6 +1021,7 @@ test('row', async () => { await run([ 'row', 'row-span', + 'row-span--1', '-row-span-4', 'row-span-unknown', 'row-auto/foo', @@ -1019,6 +1061,7 @@ test('row-start', async () => { expect( await run([ 'row-start', + 'row-start--1', 'row-start-unknown', 'row-start-auto/foo', 'row-start-4/foo', @@ -1055,6 +1098,7 @@ test('row-end', async () => { expect( await run([ 'row-end', + 'row-end--1', 'row-end-unknown', 'row-end-auto/foo', 'row-end-4/foo', @@ -1657,6 +1701,7 @@ test('line-clamp', async () => { expect( await run([ 'line-clamp', + 'line-clamp--4', '-line-clamp-4', '-line-clamp-[123]', '-line-clamp-none', @@ -1852,6 +1897,9 @@ test('aspect-ratio', async () => { 'aspect-video/foo', 'aspect-[10/9]/foo', 'aspect-4/3/foo', + 'aspect--4/3', + 'aspect--4/-3', + 'aspect-4/-3', ]), ).toEqual('') }) @@ -1924,6 +1972,10 @@ test('size', async () => { expect( await run([ 'size', + 'size--1', + 'size--1/2', + 'size--1/-2', + 'size-1/-2', '-size-4', '-size-1/2', '-size-[4px]', @@ -2026,6 +2078,10 @@ test('width', async () => { expect( await run([ 'w', + 'w--1', + 'w--1/2', + 'w--1/-2', + 'w-1/-2', '-w-4', '-w-1/2', '-w-[4px]', @@ -2275,6 +2331,10 @@ test('height', async () => { await run([ 'h', '-h-4', + 'h--1', + 'h--1/2', + 'h--1/-2', + 'h-1/-2', '-h-1/2', '-h-[4px]', 'h-full/foo', @@ -2520,6 +2580,7 @@ test('flex', async () => { expect( await run([ '-flex-1', + 'flex--1', '-flex-auto', '-flex-initial', '-flex-none', @@ -2527,6 +2588,9 @@ test('flex', async () => { 'flex-unknown', 'flex-1/foo', 'flex-99/foo', + 'flex--1/2', + 'flex--1/-2', + 'flex-1/-2', 'flex-1/2/foo', 'flex-auto/foo', 'flex-initial/foo', @@ -2553,6 +2617,8 @@ test('flex-shrink', async () => { expect( await run([ '-shrink', + 'shrink--1', + 'shrink-1.5', '-shrink-0', '-shrink-[123]', 'shrink-unknown', @@ -2580,6 +2646,8 @@ test('flex-grow', async () => { expect( await run([ '-grow', + 'grow--1', + 'grow-1.5', '-grow-0', '-grow-[123]', 'grow-unknown', @@ -2629,6 +2697,10 @@ test('flex-basis', async () => { expect( await run([ 'basis', + 'basis--1', + 'basis--1/2', + 'basis--1/-2', + 'basis-1/-2', '-basis-full', '-basis-[123px]', 'basis-auto/foo', @@ -3100,6 +3172,10 @@ test('translate', async () => { expect( await run([ 'translate', + 'translate--1', + 'translate--1/2', + 'translate--1/-2', + 'translate-1/-2', 'translate-1/2/foo', 'translate-full/foo', '-translate-full/foo', @@ -3169,6 +3245,10 @@ test('translate-x', async () => { expect( await run([ 'translate-x', + 'translate-x--1', + 'translate-x--1/2', + 'translate-x--1/-2', + 'translate-x-1/-2', 'translate-x-full/foo', '-translate-x-full/foo', 'translate-x-px/foo', @@ -3237,6 +3317,10 @@ test('translate-y', async () => { expect( await run([ 'translate-y', + 'translate-y--1', + 'translate-y--1/2', + 'translate-y--1/-2', + 'translate-y-1/-2', 'translate-y-full/foo', '-translate-y-full/foo', 'translate-y-px/foo', @@ -3288,6 +3372,10 @@ test('translate-z', async () => { expect( await run([ 'translate-z', + 'translate-z--1', + 'translate-z--1/2', + 'translate-z--1/-2', + 'translate-z-1/-2', 'translate-z-full', '-translate-z-full', 'translate-z-1/2', @@ -3357,6 +3445,7 @@ test('rotate', async () => { await run([ 'rotate', 'rotate-z', + 'rotate--2', 'rotate-unknown', 'rotate-45/foo', '-rotate-45/foo', @@ -3428,6 +3517,7 @@ test('rotate-x', async () => { expect( await run([ 'rotate-x', + 'rotate-x--1', '-rotate-x', 'rotate-x-potato', 'rotate-x-45/foo', @@ -3499,6 +3589,7 @@ test('rotate-y', async () => { expect( await run([ 'rotate-y', + 'rotate-y--1', '-rotate-y', 'rotate-y-potato', 'rotate-y-45/foo', @@ -3571,7 +3662,14 @@ test('skew', async () => { }" `) expect( - await run(['skew', 'skew-unknown', 'skew-6/foo', '-skew-6/foo', 'skew-[123deg]/foo']), + await run([ + 'skew', + 'skew--1', + 'skew-unknown', + 'skew-6/foo', + '-skew-6/foo', + 'skew-[123deg]/foo', + ]), ).toEqual('') }) @@ -3635,7 +3733,14 @@ test('skew-x', async () => { }" `) expect( - await run(['skew-x', 'skew-x-unknown', 'skew-x-6/foo', '-skew-x-6/foo', 'skew-x-[123deg]/foo']), + await run([ + 'skew-x', + 'skew-x--1', + 'skew-x-unknown', + 'skew-x-6/foo', + '-skew-x-6/foo', + 'skew-x-[123deg]/foo', + ]), ).toEqual('') }) @@ -3699,7 +3804,14 @@ test('skew-y', async () => { }" `) expect( - await run(['skew-y', 'skew-y-unknown', 'skew-y-6/foo', '-skew-y-6/foo', 'skew-y-[123deg]/foo']), + await run([ + 'skew-y', + 'skew-y--1', + 'skew-y-unknown', + 'skew-y-6/foo', + '-skew-y-6/foo', + 'skew-y-[123deg]/foo', + ]), ).toEqual('') }) @@ -3759,6 +3871,8 @@ test('scale', async () => { expect( await run([ 'scale', + 'scale--50', + 'scale-1.5', 'scale-unknown', 'scale-50/foo', '-scale-50/foo', @@ -3894,6 +4008,8 @@ test('scale-x', async () => { expect( await run([ 'scale-x', + 'scale-x--1', + 'scale-x-1.5', 'scale-x-unknown', 'scale-200/foo', 'scale-x-400/foo', @@ -3952,6 +4068,8 @@ test('scale-y', async () => { expect( await run([ 'scale-y', + 'scale-y--1', + 'scale-y-1.5', 'scale-y-unknown', 'scale-y-50/foo', '-scale-y-50/foo', @@ -4006,7 +4124,14 @@ test('scale-z', async () => { }" `) expect( - await run(['scale-z', 'scale-z-50/foo', '-scale-z-50/foo', 'scale-z-[123deg]/foo']), + await run([ + 'scale-z', + 'scale-z--1', + 'scale-z-1.5', + 'scale-z-50/foo', + '-scale-z-50/foo', + 'scale-z-[123deg]/foo', + ]), ).toEqual('') }) @@ -5767,6 +5892,7 @@ test('columns', async () => { expect( await run([ 'columns', + 'columns--4', '-columns-4', '-columns-[123]', '-columns-[--value]', @@ -6128,6 +6254,7 @@ test('grid-cols', async () => { 'grid-cols', '-grid-cols-none', '-grid-cols-subgrid', + 'grid-cols--12', '-grid-cols-12', '-grid-cols-[123]', 'grid-cols-unknown', @@ -6175,6 +6302,7 @@ test('grid-rows', async () => { 'grid-rows', '-grid-rows-none', '-grid-rows-subgrid', + 'grid-rows--12', '-grid-rows-12', '-grid-rows-[123]', 'grid-rows-unknown', @@ -6868,6 +6996,7 @@ test('divide-x', async () => { expect( await run([ '-divide-x', + 'divide-x--4', '-divide-x-4', '-divide-x-123', 'divide-x-unknown', @@ -6986,6 +7115,7 @@ test('divide-y', async () => { expect( await run([ '-divide-y', + 'divide-y--4', '-divide-y-4', '-divide-y-123', 'divide-y-unknown', @@ -7203,6 +7333,7 @@ test('accent', async () => { await run([ 'accent', '-accent-red-500', + 'accent-red-500/-50', '-accent-red-500/50', '-accent-red-500/[0.5]', '-accent-red-500/[50%]', @@ -7212,6 +7343,7 @@ test('accent', async () => { '-accent-current/[50%]', '-accent-inherit', '-accent-transparent', + 'accent-[#0088cc]/-50', '-accent-[#0088cc]', '-accent-[#0088cc]/50', '-accent-[#0088cc]/[0.5]', @@ -9819,6 +9951,8 @@ test('from', async () => { await run([ 'from', 'from-123', + 'from--123', + 'from--5%', 'from-unknown', 'from-unknown%', @@ -10056,6 +10190,8 @@ test('via', async () => { await run([ 'via', 'via-123', + 'via--123', + 'via--5%', 'via-unknown', 'via-unknown%', @@ -10281,6 +10417,8 @@ test('to', async () => { await run([ 'to', 'to-123', + 'to--123', + 'to--5%', 'to-unknown', 'to-unknown%', @@ -10895,6 +11033,7 @@ test('stroke', async () => { // Width '-stroke-0', + 'stroke--1', ]), ).toEqual('') }) @@ -11917,6 +12056,7 @@ test('decoration', async () => { '-decoration-wavy', // text-decoration-thickness + 'decoration--2', '-decoration-auto', '-decoration-from-font', '-decoration-0', @@ -12248,28 +12388,35 @@ test('filter', async () => { '-filter-[--value]', '-blur-xl', '-blur-[4px]', + 'brightness--50', '-brightness-50', '-brightness-[1.23]', 'brightness-unknown', + 'contrast--50', '-contrast-50', '-contrast-[1.23]', 'contrast-unknown', '-grayscale', '-grayscale-0', + 'grayscale--1', '-grayscale-[--value]', 'grayscale-unknown', + 'hue-rotate--5', 'hue-rotate-unknown', '-invert', + 'invert--5', '-invert-0', '-invert-[--value]', 'invert-unknown', '-drop-shadow-xl', '-drop-shadow-[0_0_red]', '-saturate-0', + 'saturate--5', '-saturate-[1.75]', '-saturate-[--value]', 'saturate-saturate', '-sepia', + 'sepia--50', '-sepia-0', '-sepia-[50%]', '-sepia-[--value]', @@ -12597,29 +12744,36 @@ test('backdrop-filter', async () => { '-backdrop-filter-[--value]', '-backdrop-blur-xl', '-backdrop-blur-[4px]', + 'backdrop-brightness--50', '-backdrop-brightness-50', '-backdrop-brightness-[1.23]', 'backdrop-brightness-unknown', + 'backdrop-contrast--50', '-backdrop-contrast-50', '-backdrop-contrast-[1.23]', 'backdrop-contrast-unknown', '-backdrop-grayscale', + 'backdrop-grayscale--1', '-backdrop-grayscale-0', '-backdrop-grayscale-[--value]', 'backdrop-grayscale-unknown', 'backdrop-hue-rotate-unknown', '-backdrop-invert', + 'backdrop-invert--1', '-backdrop-invert-0', '-backdrop-invert-[--value]', 'backdrop-invert-unknown', + 'backdrop-opacity--50', '-backdrop-opacity-50', '-backdrop-opacity-[0.5]', 'backdrop-opacity-unknown', '-backdrop-saturate-0', + 'backdrop-saturate--50', '-backdrop-saturate-[1.75]', '-backdrop-saturate-[--value]', 'backdrop-saturate-unknown', '-backdrop-sepia', + 'backdrop-sepia--50', '-backdrop-sepia-0', '-backdrop-sepia-[50%]', '-backdrop-sepia-[--value]', @@ -12637,6 +12791,7 @@ test('backdrop-filter', async () => { 'backdrop-grayscale/foo', 'backdrop-grayscale-0/foo', 'backdrop-grayscale-[--value]/foo', + 'backdrop-hue-rotate--15', 'backdrop-hue-rotate-15/foo', 'backdrop-hue-rotate-[45deg]/foo', 'backdrop-invert/foo', @@ -12789,6 +12944,7 @@ test('delay', async () => { expect( await run([ 'delay', + 'delay--200', '-delay-200', '-delay-[300ms]', 'delay-unknown', @@ -12816,6 +12972,7 @@ test('duration', async () => { expect( await run([ 'duration', + 'duration--200', '-duration-200', '-duration-[300ms]', 'duration-123/foo', @@ -13543,6 +13700,7 @@ test('outline', async () => { // outline-width '-outline-0', + 'outline--10', 'outline/foo', 'outline-none/foo', @@ -13582,6 +13740,7 @@ test('outline-offset', async () => { expect( await run([ 'outline-offset', + 'outline-offset--4', 'outline-offset-unknown', 'outline-offset-4/foo', '-outline-offset-4/foo', @@ -13604,6 +13763,7 @@ test('opacity', async () => { expect( await run([ 'opacity', + 'opacity--15', '-opacity-15', '-opacity-[--value]', 'opacity-unknown', @@ -13663,6 +13823,7 @@ test('underline-offset', async () => { expect( await run([ 'underline-offset', + 'underline-offset--4', '-underline-offset-auto', 'underline-offset-unknown', 'underline-offset-auto/foo', @@ -14642,6 +14803,7 @@ test('ring', async () => { // ring width '-ring', + 'ring--1', '-ring-0', '-ring-1', '-ring-2', @@ -14901,6 +15063,7 @@ test('inset-ring', async () => { // ring width '-inset-ring', + 'inset-ring--1', '-inset-ring-0', '-inset-ring-1', '-inset-ring-2', @@ -15066,6 +15229,7 @@ test('ring-offset', async () => { '-ring-offset-[#0088cc]/[50%]', // ring width + 'ring-offset--1', '-ring-offset-0', '-ring-offset-1', '-ring-offset-2', diff --git a/packages/tailwindcss/src/utilities.ts b/packages/tailwindcss/src/utilities.ts index 9a73c1ac0..95f8a1dd6 100644 --- a/packages/tailwindcss/src/utilities.ts +++ b/packages/tailwindcss/src/utilities.ts @@ -2,7 +2,7 @@ import { decl, rule, type AstNode, type Rule } from './ast' import type { Candidate, CandidateModifier, NamedUtilityValue } from './candidate' import type { Theme, ThemeKey } from './theme' import { DefaultMap } from './utils/default-map' -import { inferDataType } from './utils/infer-data-type' +import { inferDataType, isPositiveInteger } from './utils/infer-data-type' import { replaceShadowColors } from './utils/replace-shadow-colors' import { segment } from './utils/segment' @@ -136,7 +136,7 @@ export function asColor(value: string, modifier: CandidateModifier | null): stri return withAlpha(value, modifier.value) } - if (Number.isNaN(Number(modifier.value))) { + if (!isPositiveInteger(modifier.value)) { return null } @@ -295,7 +295,7 @@ export function createUtilities(theme: Theme) { // exist as a theme value. if (value === null && desc.supportsFractions && candidate.value.fraction) { let [lhs, rhs] = segment(candidate.value.fraction, '/') - if (!Number.isInteger(Number(lhs)) || !Number.isInteger(Number(rhs))) return + if (!isPositiveInteger(lhs) || !isPositiveInteger(rhs)) return value = `calc(${candidate.value.fraction} * 100%)` } @@ -455,7 +455,7 @@ export function createUtilities(theme: Theme) { if (!value && candidate.value.fraction) { let [lhs, rhs] = segment(candidate.value.fraction, '/') - if (!Number.isInteger(Number(lhs)) || !Number.isInteger(Number(rhs))) return + if (!isPositiveInteger(lhs) || !isPositiveInteger(rhs)) return value = `calc(${candidate.value.fraction} * 100%)` } @@ -598,7 +598,7 @@ export function createUtilities(theme: Theme) { functionalUtility('z', { supportsNegative: true, handleBareValue: ({ value }) => { - if (!Number.isInteger(Number(value))) return null + if (!isPositiveInteger(value)) return null return value }, themeKeys: ['--z-index'], @@ -622,7 +622,7 @@ export function createUtilities(theme: Theme) { functionalUtility('order', { supportsNegative: true, handleBareValue: ({ value }) => { - if (!Number.isInteger(Number(value))) return null + if (!isPositiveInteger(value)) return null return value }, themeKeys: ['--order'], @@ -648,7 +648,7 @@ export function createUtilities(theme: Theme) { staticUtility('col-span-full', [['grid-column', '1 / -1']]) functionalUtility('col-span', { handleBareValue: ({ value }) => { - if (!Number.isInteger(Number(value))) return null + if (!isPositiveInteger(value)) return null return value }, handle: (value) => [decl('grid-column', `span ${value} / span ${value}`)], @@ -661,7 +661,7 @@ export function createUtilities(theme: Theme) { functionalUtility('col-start', { supportsNegative: true, handleBareValue: ({ value }) => { - if (!Number.isInteger(Number(value))) return null + if (!isPositiveInteger(value)) return null return value }, themeKeys: ['--grid-column-start'], @@ -675,7 +675,7 @@ export function createUtilities(theme: Theme) { functionalUtility('col-end', { supportsNegative: true, handleBareValue: ({ value }) => { - if (!Number.isInteger(Number(value))) return null + if (!isPositiveInteger(value)) return null return value }, themeKeys: ['--grid-column-end'], @@ -717,7 +717,7 @@ export function createUtilities(theme: Theme) { functionalUtility('row-span', { themeKeys: [], handleBareValue: ({ value }) => { - if (!Number.isInteger(Number(value))) return null + if (!isPositiveInteger(value)) return null return value }, handle: (value) => [decl('grid-row', `span ${value} / span ${value}`)], @@ -730,7 +730,7 @@ export function createUtilities(theme: Theme) { functionalUtility('row-start', { supportsNegative: true, handleBareValue: ({ value }) => { - if (!Number.isInteger(Number(value))) return null + if (!isPositiveInteger(value)) return null return value }, themeKeys: ['--grid-row-start'], @@ -744,7 +744,7 @@ export function createUtilities(theme: Theme) { functionalUtility('row-end', { supportsNegative: true, handleBareValue: ({ value }) => { - if (!Number.isInteger(Number(value))) return null + if (!isPositiveInteger(value)) return null return value }, themeKeys: ['--grid-row-end'], @@ -839,7 +839,7 @@ export function createUtilities(theme: Theme) { functionalUtility('line-clamp', { themeKeys: ['--line-clamp'], handleBareValue: ({ value }) => { - if (!Number.isInteger(Number(value))) return null + if (!isPositiveInteger(value)) return null return value }, handle: (value) => [ @@ -893,7 +893,7 @@ export function createUtilities(theme: Theme) { handleBareValue: ({ fraction }) => { if (fraction === null) return null let [lhs, rhs] = segment(fraction, '/') - if (!Number.isInteger(Number(lhs)) || !Number.isInteger(Number(rhs))) return null + if (!isPositiveInteger(lhs) || !isPositiveInteger(rhs)) return null return fraction }, handle: (value) => [decl('aspect-ratio', value)], @@ -1068,11 +1068,11 @@ export function createUtilities(theme: Theme) { if (candidate.value.fraction) { let [lhs, rhs] = segment(candidate.value.fraction, '/') - if (!Number.isInteger(Number(lhs)) || !Number.isInteger(Number(rhs))) return + if (!isPositiveInteger(lhs) || !isPositiveInteger(rhs)) return return [decl('flex', `calc(${candidate.value.fraction} * 100%)`)] } - if (Number.isInteger(Number(candidate.value.value))) { + if (isPositiveInteger(candidate.value.value)) { if (candidate.modifier) return return [decl('flex', candidate.value.value)] } @@ -1084,7 +1084,7 @@ export function createUtilities(theme: Theme) { functionalUtility('shrink', { defaultValue: '1', handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return value }, handle: (value) => [decl('flex-shrink', value)], @@ -1096,7 +1096,7 @@ export function createUtilities(theme: Theme) { functionalUtility('grow', { defaultValue: '1', handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return value }, handle: (value) => [decl('flex-grow', value)], @@ -1337,7 +1337,7 @@ export function createUtilities(theme: Theme) { return [decl('scale', value)] } else { value = theme.resolve(candidate.value.value, ['--scale']) - if (!value && !Number.isNaN(Number(candidate.value.value))) { + if (!value && isPositiveInteger(candidate.value.value)) { value = `${candidate.value.value}%` } if (!value) return @@ -1368,7 +1368,7 @@ export function createUtilities(theme: Theme) { supportsNegative: true, themeKeys: ['--scale'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, handle: (value) => [ @@ -1420,7 +1420,7 @@ export function createUtilities(theme: Theme) { } } else { value = theme.resolve(candidate.value.value, ['--rotate']) - if (!value && !Number.isNaN(Number(candidate.value.value))) { + if (!value && isPositiveInteger(candidate.value.value)) { value = `${candidate.value.value}deg` } if (!value) return @@ -1460,7 +1460,7 @@ export function createUtilities(theme: Theme) { supportsNegative: true, themeKeys: ['--rotate'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `rotate${axis.toUpperCase()}(${value}deg)` }, handle: (value) => [ @@ -1487,7 +1487,7 @@ export function createUtilities(theme: Theme) { supportsNegative: true, themeKeys: ['--skew'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}deg` }, handle: (value) => [ @@ -1506,7 +1506,7 @@ export function createUtilities(theme: Theme) { supportsNegative: true, themeKeys: ['--skew'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}deg` }, handle: (value) => [ @@ -1524,7 +1524,7 @@ export function createUtilities(theme: Theme) { supportsNegative: true, themeKeys: ['--skew'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}deg` }, handle: (value) => [ @@ -1872,7 +1872,7 @@ export function createUtilities(theme: Theme) { functionalUtility('columns', { themeKeys: ['--columns', '--width'], handleBareValue: ({ value }) => { - if (!Number.isInteger(Number(value))) return null + if (!isPositiveInteger(value)) return null return value }, handle: (value) => [decl('columns', value)], @@ -1926,7 +1926,7 @@ export function createUtilities(theme: Theme) { functionalUtility('grid-cols', { themeKeys: ['--grid-template-columns'], handleBareValue: ({ value }) => { - if (!Number.isInteger(Number(value))) return null + if (!isPositiveInteger(value)) return null return `repeat(${value}, minmax(0, 1fr))` }, handle: (value) => [decl('grid-template-columns', value)], @@ -1937,7 +1937,7 @@ export function createUtilities(theme: Theme) { functionalUtility('grid-rows', { themeKeys: ['--grid-template-rows'], handleBareValue: ({ value }) => { - if (!Number.isInteger(Number(value))) return null + if (!isPositiveInteger(value)) return null return `repeat(${value}, minmax(0, 1fr))` }, handle: (value) => [decl('grid-template-rows', value)], @@ -2292,7 +2292,7 @@ export function createUtilities(theme: Theme) { return [borderProperties(), ...decls] } - if (!Number.isNaN(Number(candidate.value.value))) { + if (isPositiveInteger(candidate.value.value)) { let decls = desc.width(`${candidate.value.value}px`) if (!decls) return return [borderProperties(), ...decls] @@ -2394,7 +2394,7 @@ export function createUtilities(theme: Theme) { defaultValue: theme.get(['--default-border-width']) ?? '1px', themeKeys: ['--divide-width', '--border-width'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}px` }, handle: (value) => [ @@ -2414,7 +2414,7 @@ export function createUtilities(theme: Theme) { defaultValue: theme.get(['--default-border-width']) ?? '1px', themeKeys: ['--divide-width', '--border-width'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}px` }, handle: (value) => [ @@ -2672,7 +2672,7 @@ export function createUtilities(theme: Theme) { return desc.position(value) } else if ( candidate.value.value[candidate.value.value.length - 1] === '%' && - !Number.isNaN(Number(candidate.value.value.slice(0, -1))) + isPositiveInteger(candidate.value.value.slice(0, -1)) ) { return desc.position(candidate.value.value) } @@ -2841,7 +2841,7 @@ export function createUtilities(theme: Theme) { let value = theme.resolve(candidate.value.value, ['--stroke-width']) if (value) { return [decl('stroke-width', value)] - } else if (!Number.isNaN(Number(candidate.value.value))) { + } else if (isPositiveInteger(candidate.value.value)) { return [decl('stroke-width', candidate.value.value)] } } @@ -3147,7 +3147,7 @@ export function createUtilities(theme: Theme) { return [decl('text-decoration-thickness', value)] } - if (!Number.isNaN(Number(candidate.value.value))) { + if (isPositiveInteger(candidate.value.value)) { if (candidate.modifier) return return [decl('text-decoration-thickness', `${candidate.value.value}px`)] } @@ -3305,7 +3305,7 @@ export function createUtilities(theme: Theme) { functionalUtility('brightness', { themeKeys: ['--brightness'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, handle: (value) => [ @@ -3318,7 +3318,7 @@ export function createUtilities(theme: Theme) { functionalUtility('backdrop-brightness', { themeKeys: ['--backdrop-brightness', '--brightness'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, handle: (value) => [ @@ -3346,7 +3346,7 @@ export function createUtilities(theme: Theme) { functionalUtility('contrast', { themeKeys: ['--contrast'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, handle: (value) => [ @@ -3359,7 +3359,7 @@ export function createUtilities(theme: Theme) { functionalUtility('backdrop-contrast', { themeKeys: ['--backdrop-contrast', '--contrast'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, handle: (value) => [ @@ -3387,7 +3387,7 @@ export function createUtilities(theme: Theme) { functionalUtility('grayscale', { themeKeys: ['--grayscale'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, defaultValue: '100%', @@ -3401,7 +3401,7 @@ export function createUtilities(theme: Theme) { functionalUtility('backdrop-grayscale', { themeKeys: ['--backdrop-grayscale', '--grayscale'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, defaultValue: '100%', @@ -3433,7 +3433,7 @@ export function createUtilities(theme: Theme) { supportsNegative: true, themeKeys: ['--hue-rotate'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}deg` }, handle: (value) => [ @@ -3447,7 +3447,7 @@ export function createUtilities(theme: Theme) { supportsNegative: true, themeKeys: ['--backdrop-hue-rotate', '--hue-rotate'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}deg` }, handle: (value) => [ @@ -3475,7 +3475,7 @@ export function createUtilities(theme: Theme) { functionalUtility('invert', { themeKeys: ['--invert'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, defaultValue: '100%', @@ -3489,7 +3489,7 @@ export function createUtilities(theme: Theme) { functionalUtility('backdrop-invert', { themeKeys: ['--backdrop-invert', '--invert'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, defaultValue: '100%', @@ -3520,7 +3520,7 @@ export function createUtilities(theme: Theme) { functionalUtility('saturate', { themeKeys: ['--saturate'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, handle: (value) => [ @@ -3533,7 +3533,7 @@ export function createUtilities(theme: Theme) { functionalUtility('backdrop-saturate', { themeKeys: ['--backdrop-saturate', '--saturate'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, handle: (value) => [ @@ -3561,7 +3561,7 @@ export function createUtilities(theme: Theme) { functionalUtility('sepia', { themeKeys: ['--sepia'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, defaultValue: '100%', @@ -3575,7 +3575,7 @@ export function createUtilities(theme: Theme) { functionalUtility('backdrop-sepia', { themeKeys: ['--backdrop-sepia', '--sepia'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, defaultValue: '100%', @@ -3620,7 +3620,7 @@ export function createUtilities(theme: Theme) { functionalUtility('backdrop-opacity', { themeKeys: ['--backdrop-opacity', '--opacity'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, handle: (value) => [ @@ -3686,7 +3686,7 @@ export function createUtilities(theme: Theme) { functionalUtility('delay', { handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}ms` }, themeKeys: ['--transition-delay'], @@ -3713,7 +3713,7 @@ export function createUtilities(theme: Theme) { '--transition-duration', ]) - if (value === null && !Number.isNaN(Number(candidate.value.value))) { + if (value === null && isPositiveInteger(candidate.value.value)) { value = `${candidate.value.value}ms` } } @@ -4001,7 +4001,7 @@ export function createUtilities(theme: Theme) { decl('outline-style', 'var(--tw-outline-style)'), decl('outline-width', value), ] - } else if (!Number.isNaN(Number(candidate.value.value))) { + } else if (isPositiveInteger(candidate.value.value)) { return [ outlineProperties(), decl('outline-style', 'var(--tw-outline-style)'), @@ -4028,7 +4028,7 @@ export function createUtilities(theme: Theme) { supportsNegative: true, themeKeys: ['--outline-offset'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}px` }, handle: (value) => [decl('outline-offset', value)], @@ -4045,7 +4045,7 @@ export function createUtilities(theme: Theme) { functionalUtility('opacity', { themeKeys: ['--opacity'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}%` }, handle: (value) => [decl('opacity', value)], @@ -4063,7 +4063,7 @@ export function createUtilities(theme: Theme) { supportsNegative: true, themeKeys: ['--text-underline-offset'], handleBareValue: ({ value }) => { - if (Number.isNaN(Number(value))) return null + if (!isPositiveInteger(value)) return null return `${value}px` }, handle: (value) => [decl('text-underline-offset', value)], @@ -4460,7 +4460,7 @@ export function createUtilities(theme: Theme) { { if (candidate.modifier) return let value = theme.resolve(candidate.value.value, ['--ring-width']) - if (value === null && !Number.isNaN(Number(candidate.value.value))) { + if (value === null && isPositiveInteger(candidate.value.value)) { value = `${candidate.value.value}px` } if (value) { @@ -4535,7 +4535,7 @@ export function createUtilities(theme: Theme) { { if (candidate.modifier) return let value = theme.resolve(candidate.value.value, ['--ring-width']) - if (value === null && !Number.isNaN(Number(candidate.value.value))) { + if (value === null && isPositiveInteger(candidate.value.value)) { value = `${candidate.value.value}px` } if (value) { @@ -4596,7 +4596,7 @@ export function createUtilities(theme: Theme) { decl('--tw-ring-offset-width', value), decl('--tw-ring-offset-shadow', ringOffsetShadowValue), ] - } else if (!Number.isNaN(Number(candidate.value.value))) { + } else if (isPositiveInteger(candidate.value.value)) { if (candidate.modifier) return return [ decl('--tw-ring-offset-width', `${candidate.value.value}px`), diff --git a/packages/tailwindcss/src/utils/infer-data-type.ts b/packages/tailwindcss/src/utils/infer-data-type.ts index 380c08ac0..591f66173 100644 --- a/packages/tailwindcss/src/utils/infer-data-type.ts +++ b/packages/tailwindcss/src/utils/infer-data-type.ts @@ -320,3 +320,11 @@ const IS_VECTOR = new RegExp(`^${HAS_NUMBER.source} +${HAS_NUMBER.source} +${HAS function isVector(value: string) { return IS_VECTOR.test(value) } + +/** + * Returns true of the value can be parsed as a positive whole number. + */ +export function isPositiveInteger(value: any) { + let num = Number(value) + return Number.isInteger(num) && num >= 0 +} diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 40042045c..cb67e48d5 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -2252,14 +2252,18 @@ test('nth', async () => { ).toEqual('') expect( await run([ + 'nth--3:flex', 'nth-3/foo:flex', 'nth-[2n+1]/foo:flex', 'nth-[2n+1_of_.foo]/foo:flex', + 'nth-last--3:flex', 'nth-last-3/foo:flex', 'nth-last-[2n+1]/foo:flex', 'nth-last-[2n+1_of_.foo]/foo:flex', + 'nth-of-type--3:flex', 'nth-of-type-3/foo:flex', 'nth-of-type-[2n+1]/foo:flex', + 'nth-last-of-type--3:flex', 'nth-last-of-type-3/foo:flex', 'nth-last-of-type-[2n+1]/foo:flex', ]), diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 2bb9def3d..a823ec6ed 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -2,6 +2,7 @@ import { WalkAction, decl, rule, walk, type AstNode, type Rule } from './ast' import { type Variant } from './candidate' import type { Theme } from './theme' import { DefaultMap } from './utils/default-map' +import { isPositiveInteger } from './utils/infer-data-type' import { segment } from './utils/segment' type VariantFn = ( @@ -538,7 +539,7 @@ export function createVariants(theme: Theme): Variants { if (!variant.value || variant.modifier) return null // Only numeric bare values are allowed - if (variant.value.kind === 'named' && Number.isNaN(Number(variant.value.value))) return null + if (variant.value.kind === 'named' && !isPositiveInteger(variant.value.value)) return null ruleNode.nodes = [rule(`&:nth-child(${variant.value.value})`, ruleNode.nodes)] }) @@ -547,7 +548,7 @@ export function createVariants(theme: Theme): Variants { if (!variant.value || variant.modifier) return null // Only numeric bare values are allowed - if (variant.value.kind === 'named' && Number.isNaN(Number(variant.value.value))) return null + if (variant.value.kind === 'named' && !isPositiveInteger(variant.value.value)) return null ruleNode.nodes = [rule(`&:nth-last-child(${variant.value.value})`, ruleNode.nodes)] }) @@ -556,7 +557,7 @@ export function createVariants(theme: Theme): Variants { if (!variant.value || variant.modifier) return null // Only numeric bare values are allowed - if (variant.value.kind === 'named' && Number.isNaN(Number(variant.value.value))) return null + if (variant.value.kind === 'named' && !isPositiveInteger(variant.value.value)) return null ruleNode.nodes = [rule(`&:nth-of-type(${variant.value.value})`, ruleNode.nodes)] }) @@ -565,7 +566,7 @@ export function createVariants(theme: Theme): Variants { if (!variant.value || variant.modifier) return null // Only numeric bare values are allowed - if (variant.value.kind === 'named' && Number.isNaN(Number(variant.value.value))) return null + if (variant.value.kind === 'named' && !isPositiveInteger(variant.value.value)) return null ruleNode.nodes = [rule(`&:nth-last-of-type(${variant.value.value})`, ruleNode.nodes)] })