Disallow negative bare values (#14453)

Right now, it is possible to type `grid-cols--8` which maps to:

```css
/* Specificity: (0, 1, 0) */
.grid-cols--8 {
  grid-template-columns: repeat(-8, minmax(0, 1fr));
}
```

This doesn't make sense so we used this opportunity to audit all
variants and utilities and properly disallow negative bare values.
Utilities where negative values are supported still work by using the
negative utility syntax, e.g.: `-inset-4`.
This commit is contained in:
Philipp Spiess 2024-09-18 16:49:27 +02:00 committed by GitHub
parent ee7e02b1f3
commit 6ca8cc6f02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 252 additions and 73 deletions

View File

@ -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

View File

@ -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))`
}
})

View File

@ -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',

View File

@ -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`),

View File

@ -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
}

View File

@ -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',
]),

View File

@ -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<T extends Variant['kind']> = (
@ -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)]
})