Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support all color values from CSS Colors Level 4 spec #5154

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 11 additions & 65 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@
"bytes": "^3.0.0",
"chalk": "^4.1.1",
"chokidar": "^3.5.2",
"color": "^3.2.0",
"cosmiconfig": "^7.0.0",
"culori": "^0.19.1",
"detective": "^5.2.0",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
Expand Down
9 changes: 2 additions & 7 deletions src/util/pluginUtils.js
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -203,12 +203,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 = {}) {
Expand Down
80 changes: 44 additions & 36 deletions src/util/withAlphaVariable.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,42 @@
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) {
if (_.isFunction(color)) {
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 }) {
Expand All @@ -44,24 +47,29 @@ export default function withAlphaVariable({ color, property, variable }) {
}
}

try {
const isHSL = color.startsWith('hsl')

const [i, j, k, a] = isHSL ? toHsla(color) : toRgba(color)
if (isValidColor(color)) {
const parsed = culori.parse(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,
}
}
48 changes: 48 additions & 0 deletions tests/withAlphaVariable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)',
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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%)',
Expand All @@ -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))',
})
})