Skip to content

Commit

Permalink
Improve type checking for formal syntax (#9448)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
4 people committed Sep 29, 2022
1 parent 94d6e72 commit 727de66
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/corePlugins.js
Expand Up @@ -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 }) => {
Expand All @@ -1503,7 +1503,7 @@ export let corePlugins = {
},

backgroundPosition: createUtilityPlugin('backgroundPosition', [['bg', ['background-position']]], {
type: ['lookup', 'position'],
type: ['lookup', ['position', { preferOnConflict: true }]],
}),

backgroundRepeat: ({ addUtilities }) => {
Expand Down
22 changes: 11 additions & 11 deletions src/util/dataTypes.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = [
Expand All @@ -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'])
Expand Down
2 changes: 2 additions & 0 deletions src/util/pluginUtils.js
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions src/util/validateFormalSyntax.js
@@ -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')
})
}
78 changes: 76 additions & 2 deletions tests/arbitrary-values.test.js
Expand Up @@ -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;
}
`)
})
Expand All @@ -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'] })
},
],
}
Expand Down Expand Up @@ -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%;
}
`)
})
})

0 comments on commit 727de66

Please sign in to comment.