diff --git a/src/corePlugins.js b/src/corePlugins.js index 586aee7449fe..4fd2bf0ab765 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -1481,7 +1481,7 @@ export let corePlugins = { }, backgroundSize: createUtilityPlugin('backgroundSize', [['bg', ['background-size']]], { - type: ['lookup', 'length', 'percentage'], + type: ['lookup', 'length', 'percentage', 'size'], }), backgroundAttachment: ({ 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 61a401822bf1..cfeb23bc9c3d 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 @@ 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..bf56acdcc122 --- /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) { + const keywordValues = ['cover', 'contain'] + // the type will probably be a css function + // so we have to use `splitAtTopLevelOnly` + return splitAtTopLevelOnly(value, ',').every((part) => { + const 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 baea4ddef9c5..b5b4d2d4ca4c 100644 --- a/tests/arbitrary-values.test.js +++ b/tests/arbitrary-values.test.js @@ -416,3 +416,64 @@ 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.trim()).toHaveLength(0) + }) +}) + +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(` + .bg-\\[size\\:50px_10\\%\\] { + background-size: 50px 10%; + } + .bg-\\[position\\:50\\%_10\\%\\] { + background-position: 50% 10%; + } + `) + }) +})