diff --git a/package-lock.json b/package-lock.json index 30c8c7dcc..250eb6eae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "arg": "^5.0.1", "chalk": "^4.1.2", "chokidar": "^3.5.2", - "color": "^4.0.1", "cosmiconfig": "^7.0.1", + "culori": "^0.19.1", "detective": "^5.2.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", @@ -3434,15 +3434,6 @@ "node": ">=0.10.0" } }, - "node_modules/color": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/color/-/color-4.0.1.tgz", - "integrity": "sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.6.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3459,15 +3450,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/color-string": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", - "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/colord": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/colord/-/colord-2.7.0.tgz", @@ -3828,6 +3810,11 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, + "node_modules/culori": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/culori/-/culori-0.19.1.tgz", + "integrity": "sha512-K/NLpdtNnSQwH2Ru/Fk39wDL40v9PxTBFY6jHQegJDhmBqrE/d9mJB/AD4odSZJml10AlJjZdm6+I9JM3nE/EQ==" + }, "node_modules/data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -9121,19 +9108,6 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -12928,15 +12902,6 @@ "object-visit": "^1.0.0" } }, - "color": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/color/-/color-4.0.1.tgz", - "integrity": "sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==", - "requires": { - "color-convert": "^2.0.1", - "color-string": "^1.6.0" - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -12950,15 +12915,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "color-string": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", - "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "colord": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/colord/-/colord-2.7.0.tgz", @@ -13232,6 +13188,11 @@ } } }, + "culori": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/culori/-/culori-0.19.1.tgz", + "integrity": "sha512-K/NLpdtNnSQwH2Ru/Fk39wDL40v9PxTBFY6jHQegJDhmBqrE/d9mJB/AD4odSZJml10AlJjZdm6+I9JM3nE/EQ==" + }, "data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -17174,21 +17135,6 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - } - } - }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", diff --git a/package.json b/package.json index 18f75ee9d..d088ea811 100644 --- a/package.json +++ b/package.json @@ -70,8 +70,8 @@ "arg": "^5.0.1", "chalk": "^4.1.2", "chokidar": "^3.5.2", - "color": "^4.0.1", "cosmiconfig": "^7.0.1", + "culori": "^0.19.1", "detective": "^5.2.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js index 98cc2e082..1f371c7a1 100644 --- a/src/util/pluginUtils.js +++ b/src/util/pluginUtils.js @@ -1,6 +1,6 @@ import selectorParser from 'postcss-selector-parser' import postcss from 'postcss' -import createColor from 'color' +import * as culori from 'culori' import escapeCommas from './escapeCommas' import { withAlphaValue } from './withAlphaVariable' import isKeyframeRule from './isKeyframeRule' @@ -222,12 +222,7 @@ function splitAlpha(modifier) { } function isColor(value) { - try { - createColor(value) - return true - } catch (e) { - return false - } + return culori.parse(value) !== undefined } export function asColor(modifier, lookup = {}, tailwindConfig = {}) { diff --git a/src/util/withAlphaVariable.js b/src/util/withAlphaVariable.js index 5080cf908..1c9e8d48a 100644 --- a/src/util/withAlphaVariable.js +++ b/src/util/withAlphaVariable.js @@ -1,25 +1,8 @@ -import createColor from 'color' +import * as culori from 'culori' import _ from 'lodash' -function hasAlpha(color) { - return ( - color.startsWith('rgba(') || - color.startsWith('hsla(') || - (color.startsWith('#') && color.length === 9) || - (color.startsWith('#') && color.length === 5) - ) -} - -export function toRgba(color) { - const [r, g, b, a] = createColor(color).rgb().array() - - return [r, g, b, a === undefined && hasAlpha(color) ? 1 : a] -} - -export function toHsla(color) { - const [h, s, l, a] = createColor(color).hsl().array() - - return [h, `${s}%`, `${l}%`, a === undefined && hasAlpha(color) ? 1 : a] +function isValidColor(color) { + return culori.parse(color) !== undefined } export function withAlphaValue(color, alphaValue, defaultValue) { @@ -27,13 +10,33 @@ export function withAlphaValue(color, alphaValue, defaultValue) { return color({ opacityValue: alphaValue }) } - try { - const isHSL = color.startsWith('hsl') - const [i, j, k] = isHSL ? toHsla(color) : toRgba(color) - return `${isHSL ? 'hsla' : 'rgba'}(${i}, ${j}, ${k}, ${alphaValue})` - } catch { - return defaultValue + if (isValidColor(color)) { + // Parse color + const parsed = culori.parse(color) + + // Apply alpha value + parsed.alpha = alphaValue + + // Format string + let value + if (parsed.mode === 'hsl') { + value = culori.formatHsl(parsed) + } else { + value = culori.formatRgb(parsed) + } + + // Correctly apply CSS variable alpha value + if (typeof alphaValue === 'string' && alphaValue.startsWith('var(') && value.endsWith('NaN)')) { + value = value.replace('NaN)', `${alphaValue})`) + } + + // Color could not be formatted correctly + if (!value.includes('NaN')) { + return value + } } + + return defaultValue } export default function withAlphaVariable({ color, property, variable }) { @@ -44,24 +47,29 @@ export default function withAlphaVariable({ color, property, variable }) { } } - try { - const isHSL = color.startsWith('hsl') + if (isValidColor(color)) { + const parsed = culori.parse(color) - const [i, j, k, a] = isHSL ? toHsla(color) : toRgba(color) - - if (a !== undefined) { + if ('alpha' in parsed) { + // Has an alpha value, return color as-is return { [property]: color, } } + const formatFn = parsed.mode === 'hsl' ? 'formatHsl' : 'formatRgb' + const value = culori[formatFn]({ + ...parsed, + alpha: NaN, // intentionally set to `NaN` for replacing + }).replace('NaN)', `var(${variable}))`) + return { [variable]: '1', - [property]: `${isHSL ? 'hsla' : 'rgba'}(${i}, ${j}, ${k}, var(${variable}))`, - } - } catch (error) { - return { - [property]: color, + [property]: value, } } + + return { + [property]: color, + } } diff --git a/tests/withAlphaVariable.test.js b/tests/withAlphaVariable.test.js index bf689ed6a..34ae4487a 100644 --- a/tests/withAlphaVariable.test.js +++ b/tests/withAlphaVariable.test.js @@ -7,6 +7,16 @@ test('it adds the right custom property', () => { '--tw-text-opacity': '1', color: 'rgba(255, 0, 0, var(--tw-text-opacity))', }) + expect( + withAlphaVariable({ + color: 'hsl(240 100% 50%)', + property: 'color', + variable: '--tw-text-opacity', + }) + ).toEqual({ + '--tw-text-opacity': '1', + color: 'hsla(240, 100%, 50%, var(--tw-text-opacity))', + }) }) test('it ignores colors that cannot be parsed', () => { @@ -76,6 +86,15 @@ test('it ignores colors that already have an alpha channel', () => { ).toEqual({ 'background-color': 'rgba(255, 255, 255, 0.5)', }) + expect( + withAlphaVariable({ + color: 'rgba(255 255 255 / 0.5)', + property: 'background-color', + variable: '--tw-bg-opacity', + }) + ).toEqual({ + 'background-color': 'rgba(255 255 255 / 0.5)', + }) expect( withAlphaVariable({ color: 'hsla(240, 100%, 50%, 1)', @@ -94,6 +113,15 @@ test('it ignores colors that already have an alpha channel', () => { ).toEqual({ 'background-color': 'hsla(240, 100%, 50%, 0.5)', }) + expect( + withAlphaVariable({ + color: 'hsl(240 100% 50% / 0.5)', + property: 'background-color', + variable: '--tw-bg-opacity', + }) + ).toEqual({ + 'background-color': 'hsl(240 100% 50% / 0.5)', + }) }) test('it allows a closure to be passed', () => { @@ -130,6 +158,16 @@ test('it transforms rgb and hsl to rgba and hsla', () => { '--tw-bg-opacity': '1', 'background-color': 'rgba(50, 50, 50, var(--tw-bg-opacity))', }) + expect( + withAlphaVariable({ + color: 'rgb(50 50 50)', + property: 'background-color', + variable: '--tw-bg-opacity', + }) + ).toEqual({ + '--tw-bg-opacity': '1', + 'background-color': 'rgba(50, 50, 50, var(--tw-bg-opacity))', + }) expect( withAlphaVariable({ color: 'hsl(50, 50%, 50%)', @@ -140,4 +178,14 @@ test('it transforms rgb and hsl to rgba and hsla', () => { '--tw-bg-opacity': '1', 'background-color': 'hsla(50, 50%, 50%, var(--tw-bg-opacity))', }) + expect( + withAlphaVariable({ + color: 'hsl(50 50% 50%)', + property: 'background-color', + variable: '--tw-bg-opacity', + }) + ).toEqual({ + '--tw-bg-opacity': '1', + 'background-color': 'hsla(50, 50%, 50%, var(--tw-bg-opacity))', + }) })