From 97bc4f4a366ce06142c4bbd72d74829cc24f06be Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 27 Aug 2023 19:19:26 +0200 Subject: [PATCH 1/4] allow length and percentage labels for arbitrary sizes --- src/lib/validators.ts | 12 +++++++++--- tests/arbitrary-values.test.ts | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/lib/validators.ts b/src/lib/validators.ts index 2a4c2005..a4352cb1 100644 --- a/src/lib/validators.ts +++ b/src/lib/validators.ts @@ -39,8 +39,10 @@ export function isTshirtSize(value: string) { return tshirtUnitRegex.test(value) } +const sizeLabels = new Set(['length', 'size', 'percentage']) + export function isArbitrarySize(value: string) { - return getIsArbitraryValue(value, 'size', isNever) + return getIsArbitraryValue(value, sizeLabels, isNever) } export function isArbitraryPosition(value: string) { @@ -63,12 +65,16 @@ function isLengthOnly(value: string) { return lengthUnitRegex.test(value) } -function getIsArbitraryValue(value: string, label: string, testValue: (value: string) => boolean) { +function getIsArbitraryValue( + value: string, + label: string | Set, + testValue: (value: string) => boolean, +) { const result = arbitraryValueRegex.exec(value) if (result) { if (result[1]) { - return result[1] === label + return typeof label === 'string' ? result[1] === label : label.has(result[1]) } return testValue(result[2]!) diff --git a/tests/arbitrary-values.test.ts b/tests/arbitrary-values.test.ts index 34bad8a5..272db94b 100644 --- a/tests/arbitrary-values.test.ts +++ b/tests/arbitrary-values.test.ts @@ -65,4 +65,7 @@ test('handles ambiguous arbitrary values correctly', () => { expect(twMerge('text-2xl text-[calc(theme(fontSize.4xl)/1.125)]')).toBe( 'text-[calc(theme(fontSize.4xl)/1.125)]', ) + expect(twMerge('bg-cover bg-[percentage:30%] bg-[length:200px_100px]')).toBe( + 'bg-[length:200px_100px]', + ) }) From 8824bbe0092327f926bf05d67951c91193c16fd5 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 27 Aug 2023 20:31:58 +0200 Subject: [PATCH 2/4] replace isArbitraryUrl with isArbitraryImage --- src/lib/default-config.ts | 4 ++-- src/lib/validators.ts | 24 ++++++++++++++---------- tests/arbitrary-values.test.ts | 5 +++++ tests/public-api.test.ts | 4 ++-- tests/validators.test.ts | 25 +++++++++++++++---------- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/lib/default-config.ts b/src/lib/default-config.ts index 214ab601..8c48a964 100644 --- a/src/lib/default-config.ts +++ b/src/lib/default-config.ts @@ -2,12 +2,12 @@ import { fromTheme } from './from-theme' import { Config, DefaultClassGroupIds, DefaultThemeGroupIds } from './types' import { isAny, + isArbitraryImage, isArbitraryLength, isArbitraryNumber, isArbitraryPosition, isArbitraryShadow, isArbitrarySize, - isArbitraryUrl, isArbitraryValue, isInteger, isLength, @@ -903,7 +903,7 @@ export function getDefaultConfig() { bg: [ 'none', { 'gradient-to': ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl'] }, - isArbitraryUrl, + isArbitraryImage, ], }, ], diff --git a/src/lib/validators.ts b/src/lib/validators.ts index a4352cb1..93757b75 100644 --- a/src/lib/validators.ts +++ b/src/lib/validators.ts @@ -6,6 +6,8 @@ const lengthUnitRegex = /\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/ // Shadow always begins with x and y offset separated by underscore const shadowRegex = /^-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/ +const imageRegex = + /^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/ export function isLength(value: string) { return isNumber(value) || stringLengths.has(value) || fractionRegex.test(value) @@ -49,8 +51,10 @@ export function isArbitraryPosition(value: string) { return getIsArbitraryValue(value, 'position', isNever) } -export function isArbitraryUrl(value: string) { - return getIsArbitraryValue(value, 'url', isUrl) +const imageLabels = new Set(['image', 'url']) + +export function isArbitraryImage(value: string) { + return getIsArbitraryValue(value, imageLabels, isImage) } export function isArbitraryShadow(value: string) { @@ -61,10 +65,6 @@ export function isAny() { return true } -function isLengthOnly(value: string) { - return lengthUnitRegex.test(value) -} - function getIsArbitraryValue( value: string, label: string | Set, @@ -83,14 +83,18 @@ function getIsArbitraryValue( return false } -function isNever() { - return false +function isLengthOnly(value: string) { + return lengthUnitRegex.test(value) } -function isUrl(value: string) { - return value.startsWith('url(') +function isNever() { + return false } function isShadow(value: string) { return shadowRegex.test(value) } + +function isImage(value: string) { + return imageRegex.test(value) +} diff --git a/tests/arbitrary-values.test.ts b/tests/arbitrary-values.test.ts index 272db94b..8d485be1 100644 --- a/tests/arbitrary-values.test.ts +++ b/tests/arbitrary-values.test.ts @@ -68,4 +68,9 @@ test('handles ambiguous arbitrary values correctly', () => { expect(twMerge('bg-cover bg-[percentage:30%] bg-[length:200px_100px]')).toBe( 'bg-[length:200px_100px]', ) + expect( + twMerge( + 'bg-none bg-[url(.)] bg-[image:.] bg-[url:.] bg-[linear-gradient(.)] bg-gradient-to-r', + ), + ).toBe('bg-gradient-to-r') }) diff --git a/tests/public-api.test.ts b/tests/public-api.test.ts index eaacfb01..12deb896 100644 --- a/tests/public-api.test.ts +++ b/tests/public-api.test.ts @@ -24,7 +24,7 @@ test('has correct export types', () => { isArbitraryPosition: expect.any(Function), isArbitraryShadow: expect.any(Function), isArbitrarySize: expect.any(Function), - isArbitraryUrl: expect.any(Function), + isArbitraryImage: expect.any(Function), isArbitraryValue: expect.any(Function), isInteger: expect.any(Function), isLength: expect.any(Function), @@ -152,7 +152,7 @@ test('validators have correct inputs and outputs', () => { expect(validators.isTshirtSize('')).toEqual(expect.any(Boolean)) expect(validators.isArbitrarySize('')).toEqual(expect.any(Boolean)) expect(validators.isArbitraryPosition('')).toEqual(expect.any(Boolean)) - expect(validators.isArbitraryUrl('')).toEqual(expect.any(Boolean)) + expect(validators.isArbitraryImage('')).toEqual(expect.any(Boolean)) expect(validators.isArbitraryNumber('')).toEqual(expect.any(Boolean)) expect(validators.isArbitraryShadow('')).toEqual(expect.any(Boolean)) }) diff --git a/tests/validators.test.ts b/tests/validators.test.ts index d45cbb60..4a67630b 100644 --- a/tests/validators.test.ts +++ b/tests/validators.test.ts @@ -10,7 +10,7 @@ const { isTshirtSize, isArbitrarySize, isArbitraryPosition, - isArbitraryUrl, + isArbitraryImage, isArbitraryNumber, isArbitraryShadow, } = validators @@ -120,6 +120,8 @@ test('isTshirtSize', () => { test('isArbitrarySize', () => { expect(isArbitrarySize('[size:2px]')).toBe(true) expect(isArbitrarySize('[size:bla]')).toBe(true) + expect(isArbitrarySize('[length:bla]')).toBe(true) + expect(isArbitrarySize('[percentage:bla]')).toBe(true) expect(isArbitrarySize('[2px]')).toBe(false) expect(isArbitrarySize('[bla]')).toBe(false) @@ -135,15 +137,18 @@ test('isArbitraryPosition', () => { expect(isArbitraryPosition('position:2px')).toBe(false) }) -test('isArbitraryUrl', () => { - expect(isArbitraryUrl('[url:var(--my-url)]')).toBe(true) - expect(isArbitraryUrl('[url(something)]')).toBe(true) - expect(isArbitraryUrl('[url:bla]')).toBe(true) - - expect(isArbitraryUrl('[var(--my-url)]')).toBe(false) - expect(isArbitraryUrl('[bla]')).toBe(false) - expect(isArbitraryUrl('url:2px')).toBe(false) - expect(isArbitraryUrl('url(2px)')).toBe(false) +test('isArbitraryImage', () => { + expect(isArbitraryImage('[url:var(--my-url)]')).toBe(true) + expect(isArbitraryImage('[url(something)]')).toBe(true) + expect(isArbitraryImage('[url:bla]')).toBe(true) + expect(isArbitraryImage('[image:bla]')).toBe(true) + expect(isArbitraryImage('[linear-gradient(something)]')).toBe(true) + expect(isArbitraryImage('[repeating-conic-gradient(something)]')).toBe(true) + + expect(isArbitraryImage('[var(--my-url)]')).toBe(false) + expect(isArbitraryImage('[bla]')).toBe(false) + expect(isArbitraryImage('url:2px')).toBe(false) + expect(isArbitraryImage('url(2px)')).toBe(false) }) test('isArbitraryNumber', () => { From 25ffd531dfbe54c9571d1c05d4992cce31f532b0 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 27 Aug 2023 20:46:02 +0200 Subject: [PATCH 3/4] update documentation and migration guide --- docs/api-reference.md | 2 +- docs/changelog/v1-to-v2-migration.md | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 4338b45d..98db2685 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -339,7 +339,7 @@ A brief summary for each validator: - `isTshirtSize`checks whether class part is a T-shirt size (`sm`, `xl`), optionally with a preceding number (`2xl`). - `isArbitrarySize` checks whether class part is an arbitrary value which starts with `size:` (`[size:200px_100px]`) which is necessary for background-size classNames. - `isArbitraryPosition` checks whether class part is an arbitrary value which starts with `position:` (`[position:200px_100px]`) which is necessary for background-position classNames. -- `isArbitraryUrl` checks whether class part is an arbitrary value which starts with `url:` or `url(` (`[url('/path-to-image.png')]`, `url:var(--maybe-a-url-at-runtime)]`) which is necessary for background-image classNames. +- `isArbitraryImage` checks whether class part is an arbitrary value which is an iamge, e.g. by starting with `image:`, `url:`, `linear-gradient(` or `url(` (`[url('/path-to-image.png')]`, `image:var(--maybe-an-image-at-runtime)]`) which is necessary for background-image classNames. - `isArbitraryShadow` checks whether class part is an arbitrary value which starts with the same pattern as a shadow value (`[0_35px_60px_-15px_rgba(0,0,0,0.3)]`), namely with two lengths separated by a underscore. - `isAny` always returns true. Be careful with this validator as it might match unwanted classes. I use it primarily to match colors or when I'm certain there are no other class groups in a namespace. diff --git a/docs/changelog/v1-to-v2-migration.md b/docs/changelog/v1-to-v2-migration.md index de7e804a..5461418d 100644 --- a/docs/changelog/v1-to-v2-migration.md +++ b/docs/changelog/v1-to-v2-migration.md @@ -30,6 +30,7 @@ By exports: - `validators` - [`isLength`: Does not check for arbitrary values anymore](#validatorsislength-does-not-check-for-arbitrary-values-anymore) - [`isInteger`: Does not check for arbitrary values anymore](#validatorsisinteger-does-not-check-for-arbitrary-values-anymore) + - [`isArbitraryUrl`: Renamed](#validatorsisarbitraryurl-renamed) - [`isArbitraryWeight`: Removed](#validatorsisarbitraryweight-removed) - `createTailwindMerge` - [Mandatory elements added](#createtailwindmerge-mandatory-elements-added) @@ -301,6 +302,23 @@ If those classes use arbitrary values but there is only a single class group tha Otherwise, proceed as shown in the minimal upgrade. +### `validators.isArbitraryUrl`: Renamed + +Related: [#300](https://github.com/dcastil/tailwind-merge/pull/300) + +`isArbitraryUrl` was used to detect arbitrary `background-image` values. However, the `background-image` CSS property supports more than just URLs, so the functionality of the validator was expanded to also detect values like `image:var(--maybe-an-image-at-runtime)]` or `linear-gradient(rgba(0,0,255,0.5),rgba(255,255,0,0.5))` and therefore renamed as well. + +#### Upgrade + +Replace all uses of `validators.isArbitraryUrl` with `validators.isArbitraryImage`. + +```diff + import { validators } from 'tailwind-merge' + +- validators.isArbitraryUrl ++ validators.isArbitraryImage +``` + ### `validators.isArbitraryWeight`: Removed Related: [#288](https://github.com/dcastil/tailwind-merge/pull/288) From 742420a53b3c471d4c488bef53f2fbcaaaa8eac0 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 27 Aug 2023 20:46:37 +0200 Subject: [PATCH 4/4] forgot to update TypeScript type in docs --- docs/api-reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 98db2685..f2b90803 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -314,7 +314,7 @@ interface Validators { isTshirtSize(value: string): boolean isArbitrarySize(value: string): boolean isArbitraryPosition(value: string): boolean - isArbitraryUrl(value: string): boolean + isArbitraryImage(value: string): boolean isArbitraryNumber(value: string): boolean isArbitraryShadow(value: string): boolean isAny(value: string): boolean