diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f909b3a25de..32dc8876f5f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/corePlugins.js b/src/corePlugins.js index 3fd4ed99c51a..5cf27287128e 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -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 }) => { diff --git a/src/util/dataTypes.js b/src/util/dataTypes.js index d7f04f7044bc..beb4cec89499 100644 --- a/src/util/dataTypes.js +++ b/src/util/dataTypes.js @@ -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']) diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index d056a78cee01..c56f6a1dd412 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -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) diff --git a/src/util/validateFormalSyntax.js b/src/util/validateFormalSyntax.js new file mode 100644 index 000000000000..d3dafea1f07f --- /dev/null +++ b/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 = + * # + * + * = + * [ | auto ]{1,2} | + * cover | + * contain + * + * = + * | + * + * + * @param {string} value + */ +export function backgroundSize(value) { + let keywordValues = ['cover', 'contain'] + // the 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') + }) +} diff --git a/tests/arbitrary-values.test.js b/tests/arbitrary-values.test.js index a4dbf6fe990a..da17a6cdf1aa 100644 --- a/tests/arbitrary-values.test.js +++ b/tests/arbitrary-values.test.js @@ -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`
` }], + 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`
` }], + 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`
` }, + ], + 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%; + } + `) + }) +})