Skip to content

Commit 4919cbf

Browse files
bradlcRobinMalfait
andauthored
Update color parsing and formatting (tailwindlabs#5442)
* Replace `culori` with simple color parser * Use space-separated color syntax * Update default color values to use space-separated syntax * Update separator regex * Fix tests * add tests for the new `color` util Also slightly modified the `color` util itself to take `transparent` into account and also format every value as a string for consistency. Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
1 parent 12fa78b commit 4919cbf

25 files changed

+407
-300
lines changed

package-lock.json

Lines changed: 1 addition & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@
7070
"arg": "^5.0.1",
7171
"chalk": "^4.1.2",
7272
"chokidar": "^3.5.2",
73+
"color-name": "^1.1.4",
7374
"cosmiconfig": "^7.0.1",
74-
"culori": "^0.19.1",
7575
"detective": "^5.2.0",
7676
"didyoumean": "^1.2.2",
7777
"dlv": "^1.1.3",

src/corePlugins.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,7 +1302,7 @@ export let backgroundImage = createUtilityPlugin(
13021302
)
13031303
export let gradientColorStops = (() => {
13041304
function transparentTo(value) {
1305-
return withAlphaValue(value, 0, 'rgba(255, 255, 255, 0)')
1305+
return withAlphaValue(value, 0, 'rgb(255 255 255 / 0)')
13061306
}
13071307

13081308
return function ({ matchUtilities, theme }) {
@@ -1738,7 +1738,7 @@ export let ringWidth = ({ matchUtilities, addBase, addUtilities, theme }) => {
17381738
let ringColorDefault = withAlphaValue(
17391739
theme('ringColor.DEFAULT'),
17401740
ringOpacityDefault,
1741-
`rgba(147, 197, 253, ${ringOpacityDefault})`
1741+
`rgb(147 197 253 / ${ringOpacityDefault})`
17421742
)
17431743

17441744
addBase({

src/util/color.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import namedColors from 'color-name'
2+
3+
let HEX = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i
4+
let SHORT_HEX = /^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i
5+
let VALUE = `(?:\\d+|\\d*\\.\\d+)%?`
6+
let SEP = `(?:\\s*,\\s*|\\s+)`
7+
let ALPHA_SEP = `\\s*[,/]\\s*`
8+
let RGB_HSL = new RegExp(
9+
`^(rgb|hsl)a?\\(\\s*(${VALUE})${SEP}(${VALUE})${SEP}(${VALUE})(?:${ALPHA_SEP}(${VALUE}))?\\s*\\)$`
10+
)
11+
12+
export function parseColor(value) {
13+
if (typeof value !== 'string') {
14+
return null
15+
}
16+
17+
value = value.trim()
18+
if (value === 'transparent') {
19+
return { mode: 'rgb', color: ['0', '0', '0'], alpha: '0' }
20+
}
21+
22+
if (value in namedColors) {
23+
return { mode: 'rgb', color: namedColors[value].map((v) => v.toString()) }
24+
}
25+
26+
let hex = value
27+
.replace(SHORT_HEX, (_, r, g, b, a) => ['#', r, r, g, g, b, b, a ? a + a : ''].join(''))
28+
.match(HEX)
29+
30+
if (hex !== null) {
31+
return {
32+
mode: 'rgb',
33+
color: [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)].map((v) =>
34+
v.toString()
35+
),
36+
alpha: hex[4] ? (parseInt(hex[4], 16) / 255).toString() : undefined,
37+
}
38+
}
39+
40+
let match = value.match(RGB_HSL)
41+
42+
if (match !== null) {
43+
return {
44+
mode: match[1],
45+
color: [match[2], match[3], match[4]].map((v) => v.toString()),
46+
alpha: match[5]?.toString?.(),
47+
}
48+
}
49+
50+
return null
51+
}
52+
53+
export function formatColor({ mode, color, alpha }) {
54+
let hasAlpha = alpha !== undefined
55+
return `${mode}(${color.join(' ')}${hasAlpha ? ` / ${alpha}` : ''})`
56+
}

src/util/pluginUtils.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import selectorParser from 'postcss-selector-parser'
22
import postcss from 'postcss'
3-
import * as culori from 'culori'
43
import escapeCommas from './escapeCommas'
54
import { withAlphaValue } from './withAlphaVariable'
65
import isKeyframeRule from './isKeyframeRule'
6+
import { parseColor } from './color'
77

88
export function applyPseudoToMarker(selector, marker, state, join) {
99
let states = [state]
@@ -221,10 +221,6 @@ function splitAlpha(modifier) {
221221
return [modifier.slice(0, slashIdx), modifier.slice(slashIdx + 1)]
222222
}
223223

224-
function isColor(value) {
225-
return culori.parse(value) !== undefined
226-
}
227-
228224
export function asColor(modifier, lookup = {}, tailwindConfig = {}) {
229225
if (lookup[modifier] !== undefined) {
230226
return lookup[modifier]
@@ -245,7 +241,7 @@ export function asColor(modifier, lookup = {}, tailwindConfig = {}) {
245241
}
246242

247243
return asValue(modifier, lookup, {
248-
validate: isColor,
244+
validate: (value) => parseColor(value) !== null,
249245
})
250246
}
251247

src/util/withAlphaVariable.js

Lines changed: 15 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,17 @@
1-
import * as culori from 'culori'
2-
3-
function isValidColor(color) {
4-
return culori.parse(color) !== undefined
5-
}
1+
import { parseColor, formatColor } from './color'
62

73
export function withAlphaValue(color, alphaValue, defaultValue) {
84
if (typeof color === 'function') {
95
return color({ opacityValue: alphaValue })
106
}
117

12-
if (isValidColor(color)) {
13-
// Parse color
14-
const parsed = culori.parse(color)
15-
16-
// Apply alpha value
17-
parsed.alpha = alphaValue
18-
19-
// Format string
20-
let value
21-
if (parsed.mode === 'hsl') {
22-
value = culori.formatHsl(parsed)
23-
} else {
24-
value = culori.formatRgb(parsed)
25-
}
26-
27-
// Correctly apply CSS variable alpha value
28-
if (typeof alphaValue === 'string' && alphaValue.startsWith('var(') && value.endsWith('NaN)')) {
29-
value = value.replace('NaN)', `${alphaValue})`)
30-
}
8+
let parsed = parseColor(color)
319

32-
// Color could not be formatted correctly
33-
if (!value.includes('NaN')) {
34-
return value
35-
}
10+
if (parsed === null) {
11+
return defaultValue
3612
}
3713

38-
return defaultValue
14+
return formatColor({ ...parsed, alpha: alphaValue })
3915
}
4016

4117
export default function withAlphaVariable({ color, property, variable }) {
@@ -46,29 +22,23 @@ export default function withAlphaVariable({ color, property, variable }) {
4622
}
4723
}
4824

49-
if (isValidColor(color)) {
50-
const parsed = culori.parse(color)
25+
const parsed = parseColor(color)
5126

52-
if ('alpha' in parsed) {
53-
// Has an alpha value, return color as-is
54-
return {
55-
[property]: color,
56-
}
27+
if (parsed === null) {
28+
return {
29+
[property]: color,
5730
}
31+
}
5832

59-
const formatFn = parsed.mode === 'hsl' ? 'formatHsl' : 'formatRgb'
60-
const value = culori[formatFn]({
61-
...parsed,
62-
alpha: NaN, // intentionally set to `NaN` for replacing
63-
}).replace('NaN)', `var(${variable}))`)
64-
33+
if (parsed.alpha !== undefined) {
34+
// Has an alpha value, return color as-is
6535
return {
66-
[variable]: '1',
67-
[property]: value,
36+
[property]: color,
6837
}
6938
}
7039

7140
return {
72-
[property]: color,
41+
[variable]: '1',
42+
[property]: formatColor({ ...parsed, alpha: `var(${variable})` }),
7343
}
7444
}

stubs/defaultConfig.stub.js

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -201,13 +201,13 @@ module.exports = {
201201
8: '8px',
202202
},
203203
boxShadow: {
204-
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
205-
DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
206-
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
207-
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
208-
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
209-
'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
210-
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
204+
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
205+
DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06)',
206+
md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -1px rgb(0 0 0 / 0.06)',
207+
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -2px rgb(0 0 0 / 0.05)',
208+
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 10px 10px -5px rgb(0 0 0 / 0.04)',
209+
'2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)',
210+
inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.06)',
211211
none: 'none',
212212
},
213213
caretColor: (theme) => theme('colors'),
@@ -242,12 +242,12 @@ module.exports = {
242242
divideOpacity: (theme) => theme('borderOpacity'),
243243
divideWidth: (theme) => theme('borderWidth'),
244244
dropShadow: {
245-
sm: '0 1px 1px rgba(0,0,0,0.05)',
246-
DEFAULT: ['0 1px 2px rgba(0, 0, 0, 0.1)', '0 1px 1px rgba(0, 0, 0, 0.06)'],
247-
md: ['0 4px 3px rgba(0, 0, 0, 0.07)', '0 2px 2px rgba(0, 0, 0, 0.06)'],
248-
lg: ['0 10px 8px rgba(0, 0, 0, 0.04)', '0 4px 3px rgba(0, 0, 0, 0.1)'],
249-
xl: ['0 20px 13px rgba(0, 0, 0, 0.03)', '0 8px 5px rgba(0, 0, 0, 0.08)'],
250-
'2xl': '0 25px 25px rgba(0, 0, 0, 0.15)',
245+
sm: '0 1px 1px rgb(0 0 0 / 0.05)',
246+
DEFAULT: ['0 1px 2px rgb(0 0 0 / 0.1)', '0 1px 1px rgb(0 0 0 / 0.06)'],
247+
md: ['0 4px 3px rgb(0 0 0 / 0.07)', '0 2px 2px rgb(0 0 0 / 0.06)'],
248+
lg: ['0 10px 8px rgb(0 0 0 / 0.04)', '0 4px 3px rgb(0 0 0 / 0.1)'],
249+
xl: ['0 20px 13px rgb(0 0 0 / 0.03)', '0 8px 5px rgb(0 0 0 / 0.08)'],
250+
'2xl': '0 25px 25px rgb(0 0 0 / 0.15)',
251251
none: '0 0 #0000',
252252
},
253253
fill: { current: 'currentColor' },

tests/apply.test.css

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.basic-example {
22
border-radius: 0.375rem;
33
--tw-bg-opacity: 1;
4-
background-color: rgba(59, 130, 246, var(--tw-bg-opacity));
4+
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
55
padding-left: 1rem;
66
padding-right: 1rem;
77
padding-top: 0.5rem;
@@ -94,7 +94,7 @@
9494
.selectors {
9595
border-radius: 0.375rem;
9696
--tw-bg-opacity: 1;
97-
background-color: rgba(59, 130, 246, var(--tw-bg-opacity));
97+
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
9898
padding-left: 1rem;
9999
padding-right: 1rem;
100100
padding-top: 0.5rem;
@@ -142,12 +142,12 @@
142142
var(--tw-numeric-spacing) var(--tw-numeric-fraction);
143143
--tw-ordinal: ordinal;
144144
--tw-numeric-spacing: tabular-nums;
145-
--tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
145+
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -2px rgb(0 0 0 / 0.05);
146146
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
147147
var(--tw-shadow);
148148
}
149149
.complex-utilities:hover {
150-
--tw-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
150+
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 10px 10px -5px rgb(0 0 0 / 0.04);
151151
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
152152
var(--tw-shadow);
153153
}
@@ -192,13 +192,13 @@
192192
padding-right: 1rem;
193193
font-weight: 700;
194194
--tw-bg-opacity: 1;
195-
background-color: rgba(59, 130, 246, var(--tw-bg-opacity));
195+
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
196196
--tw-text-opacity: 1;
197-
color: rgba(255, 255, 255, var(--tw-text-opacity));
197+
color: rgb(255 255 255 / var(--tw-text-opacity));
198198
}
199199
.btn-blue:hover {
200200
--tw-bg-opacity: 1;
201-
background-color: rgba(29, 78, 216, var(--tw-bg-opacity));
201+
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
202202
}
203203
.recursive-apply-a {
204204
font-weight: 900;

tests/apply.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,25 +293,25 @@ test('@apply classes from outside a @layer', async () => {
293293
294294
.bar {
295295
--tw-text-opacity: 1;
296-
color: rgba(239, 68, 68, var(--tw-text-opacity));
296+
color: rgb(239 68 68 / var(--tw-text-opacity));
297297
font-weight: 700;
298298
}
299299
300300
.bar:hover {
301301
--tw-text-opacity: 1;
302-
color: rgba(34, 197, 94, var(--tw-text-opacity));
302+
color: rgb(34 197 94 / var(--tw-text-opacity));
303303
}
304304
305305
.baz {
306306
text-decoration: underline;
307307
--tw-text-opacity: 1;
308-
color: rgba(239, 68, 68, var(--tw-text-opacity));
308+
color: rgb(239 68 68 / var(--tw-text-opacity));
309309
font-weight: 700;
310310
}
311311
312312
.baz:hover {
313313
--tw-text-opacity: 1;
314-
color: rgba(34, 197, 94, var(--tw-text-opacity));
314+
color: rgb(34 197 94 / var(--tw-text-opacity));
315315
}
316316
317317
.keep-me-even-though-I-am-not-used-in-content {

0 commit comments

Comments
 (0)