Improve type checking for formal syntax (#9448)

* Improve type checking for formal syntax

* Add test

* Change order of test class name

* fix failing tests

* prefer `position` over `size` for backwards compatibility reasons

Previously `bg-[10px_10%]` generated `background-position: 10px 10%` before we introduced the fallback plugins.
Therefore we should prefer `position` over `size` as the default for backwards compatibility.

* update changelog

* ensure correct order

Thanks Prettier!

* update changelog

Co-authored-by: lzt1008 <lzt1008@live.com>
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Co-authored-by: liangzhengtai <liangzhengtai_i@didiglobal.com>
This commit is contained in:
Robin Malfait 2022-09-29 18:24:51 +02:00 committed by GitHub
parent 94d6e7299a
commit 727de668fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 126 additions and 15 deletions

View File

@ -41,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Revert change that only listened for stdin close on TTYs ([#9331](https://github.com/tailwindlabs/tailwindcss/pull/9331))
- Ignore unset values (like `null` or `undefined`) when resolving the classList for intellisense ([#9385](https://github.com/tailwindlabs/tailwindcss/pull/9385))
- Implement fallback plugins when arbitrary values result in css from multiple plugins ([#9376](https://github.com/tailwindlabs/tailwindcss/pull/9376))
- Improve type checking for formal syntax ([#9349](https://github.com/tailwindlabs/tailwindcss/pull/9349), [#9448](https://github.com/tailwindlabs/tailwindcss/pull/9448))
## [3.1.8] - 2022-08-05

View File

@ -1482,7 +1482,7 @@ export let corePlugins = {
},
backgroundSize: createUtilityPlugin('backgroundSize', [['bg', ['background-size']]], {
type: ['lookup', ['length', { preferOnConflict: true }], 'percentage'],
type: ['lookup', 'length', 'percentage', 'size'],
}),
backgroundAttachment: ({ addUtilities }) => {
@ -1503,7 +1503,7 @@ export let corePlugins = {
},
backgroundPosition: createUtilityPlugin('backgroundPosition', [['bg', ['background-position']]], {
type: ['lookup', 'position'],
type: ['lookup', ['position', { preferOnConflict: true }]],
}),
backgroundRepeat: ({ addUtilities }) => {

View File

@ -6,6 +6,10 @@ let cssFunctions = ['min', 'max', 'clamp', 'calc']
// Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types
function isCSSFunction(value) {
return cssFunctions.some((fn) => new RegExp(`^${fn}\\(.*\\)`).test(value))
}
// This is not a data type, but rather a function that can normalize the
// correct values.
export function normalize(value, isRoot = true) {
@ -55,13 +59,11 @@ export function url(value) {
}
export function number(value) {
return !isNaN(Number(value)) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?`).test(value))
return !isNaN(Number(value)) || isCSSFunction(value)
}
export function percentage(value) {
return splitAtTopLevelOnly(value, '_').every((part) => {
return /%$/g.test(part) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?%`).test(part))
})
return (value.endsWith('%') && number(value.slice(0, -1))) || isCSSFunction(value)
}
let lengthUnits = [
@ -84,13 +86,11 @@ let lengthUnits = [
]
let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
export function length(value) {
return splitAtTopLevelOnly(value, '_').every((part) => {
return (
part === '0' ||
new RegExp(`${lengthUnitsPattern}$`).test(part) ||
cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?${lengthUnitsPattern}`).test(part))
)
})
return (
value === '0' ||
new RegExp(`^[+-]?[0-9]*\.?[0-9]+(?:[eE][+-]?[0-9]+)?${lengthUnitsPattern}$`).test(value) ||
isCSSFunction(value)
)
}
let lineWidths = new Set(['thin', 'medium', 'thick'])

View File

@ -18,6 +18,7 @@ import {
shadow,
} from './dataTypes'
import negateValue from './negateValue'
import { backgroundSize } from './validateFormalSyntax'
export function updateAllClasses(selectors, updateClass) {
let parser = selectorParser((selectors) => {
@ -162,6 +163,7 @@ export let typeMap = {
'absolute-size': guess(absoluteSize),
'relative-size': guess(relativeSize),
shadow: guess(shadow),
size: guess(backgroundSize),
}
let supportedTypes = Object.keys(typeMap)

View File

@ -0,0 +1,34 @@
import { length, percentage } from './dataTypes'
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
/**
*
* https://developer.mozilla.org/en-US/docs/Web/CSS/background-size#formal_syntax
*
* background-size =
* <bg-size>#
*
* <bg-size> =
* [ <length-percentage [0,]> | auto ]{1,2} |
* cover |
* contain
*
* <length-percentage> =
* <length> |
* <percentage>
*
* @param {string} value
*/
export function backgroundSize(value) {
let keywordValues = ['cover', 'contain']
// the <length-percentage> type will probably be a css function
// so we have to use `splitAtTopLevelOnly`
return splitAtTopLevelOnly(value, ',').every((part) => {
let sizes = splitAtTopLevelOnly(part, '_').filter(Boolean)
if (sizes.length === 1 && keywordValues.includes(sizes[0])) return true
if (sizes.length !== 1 && sizes.length !== 2) return false
return sizes.every((size) => length(size) || percentage(size) || size === 'auto')
})
}

View File

@ -285,7 +285,7 @@ it('should pick the fallback plugin when arbitrary values collide', () => {
}
.bg-\[200px_100px\] {
background-size: 200px 100px;
background-position: 200px 100px;
}
`)
})
@ -311,7 +311,7 @@ it('should warn and not generate if arbitrary values are ambiguous (without fall
plugins: [
function ({ matchUtilities }) {
matchUtilities({ foo: (value) => ({ value }) }, { type: ['position'] })
matchUtilities({ foo: (value) => ({ value }) }, { type: ['length'] })
matchUtilities({ foo: (value) => ({ value }) }, { type: ['size'] })
},
],
}
@ -463,3 +463,77 @@ it('should correctly validate each part when checking for `percentage` data type
`)
})
})
it('should correctly validate background size', () => {
let config = {
content: [{ raw: html`<div class="bg-[auto_auto,cover,_contain,10px,10px_10%]"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[auto_auto\2c cover\2c _contain\2c 10px\2c 10px_10\%\] {
background-size: auto auto, cover, contain, 10px, 10px 10%;
}
`)
})
})
it('should correctly validate combination of percentage and length', () => {
let config = {
content: [{ raw: html`<div class="bg-[50px_10%] bg-[50%_10%] bg-[50px_10px]"></div>` }],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[50px_10\%\] {
background-position: 50px 10%;
}
.bg-\[50\%_10\%\] {
background-position: 50% 10%;
}
.bg-\[50px_10px\] {
background-position: 50px 10px;
}
`)
})
})
it('can explicitly specify type for percentage and length', () => {
let config = {
content: [
{ raw: html`<div class="bg-[size:50px_10%] bg-[50px_10px] bg-[position:50%_10%]"></div>` },
],
corePlugins: { preflight: false },
plugins: [],
}
let input = css`
@tailwind utilities;
`
return run(input, config).then((result) => {
expect(result.css).toMatchFormattedCss(css`
.bg-\[size\:50px_10\%\] {
background-size: 50px 10%;
}
.bg-\[50px_10px\] {
background-position: 50px 10px;
}
.bg-\[position\:50\%_10\%\] {
background-position: 50% 10%;
}
`)
})
})