Skip to content

Commit

Permalink
Support alpha values for theme() function (#8416)
Browse files Browse the repository at this point in the history
* Fix typo

* Support alpha modifier for theme color values

* Eliminate redundant object creation in resolveFunctionKeys

Building an object of N keys incrementally using Object.reduce + splat results in N intermediate objects. We should just create one object and assign each key.

* Switch to inline theme values in theme fn in config

* Add test case

And fix typos that were definitely not there

* Update changelog
  • Loading branch information
thecrypticace committed May 25, 2022
1 parent 22f9dc8 commit 50bed74
Show file tree
Hide file tree
Showing 8 changed files with 540 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `matchVariant` API ([#8310](https://github.com/tailwindlabs/tailwindcss/pull/8310))
- Add `prefers-contrast` media query variants ([#8410](https://github.com/tailwindlabs/tailwindcss/pull/8410))
- Experimental support for variant grouping ([#8405](https://github.com/tailwindlabs/tailwindcss/pull/8405))
- Add opacity support when referencing colors with `theme` function ([#8416](https://github.com/tailwindlabs/tailwindcss/pull/8416))

## [3.0.24] - 2022-04-12

Expand Down
22 changes: 18 additions & 4 deletions src/lib/evaluateTailwindFunctions.js
Expand Up @@ -5,6 +5,7 @@ import parseValue from 'postcss-value-parser'
import { normalizeScreens } from '../util/normalizeScreens'
import buildMediaQuery from '../util/buildMediaQuery'
import { toPath } from '../util/toPath'
import { withAlphaValue } from '../util/withAlphaVariable'

function isObject(input) {
return typeof input === 'object' && input !== null
Expand Down Expand Up @@ -37,7 +38,7 @@ function listKeys(obj) {
return list(Object.keys(obj))
}

function validatePath(config, path, defaultValue) {
function validatePath(config, path, defaultValue, themeOpts = {}) {
const pathString = Array.isArray(path)
? pathToString(path)
: path.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
Expand Down Expand Up @@ -114,7 +115,7 @@ function validatePath(config, path, defaultValue) {

return {
isValid: true,
value: transformThemeValue(themeSection)(value),
value: transformThemeValue(themeSection)(value, themeOpts),
}
}

Expand Down Expand Up @@ -160,16 +161,29 @@ let nodeTypePropertyMap = {
export default function ({ tailwindConfig: config }) {
let functions = {
theme: (node, path, ...defaultValue) => {
const { isValid, value, error } = validatePath(
let matches = path.match(/^([^\/\s]+)(?:\s*\/\s*([^\/\s]+))$/)
let alpha = undefined

if (matches) {
path = matches[1]
alpha = matches[2]
}

let { isValid, value, error } = validatePath(
config,
path,
defaultValue.length ? defaultValue : undefined
defaultValue.length ? defaultValue : undefined,
{ opacityValue: alpha }
)

if (!isValid) {
throw node.error(error)
}

if (alpha !== undefined) {
value = withAlphaValue(value, alpha, value)
}

return value
},
screen: (node, screen) => {
Expand Down
19 changes: 14 additions & 5 deletions src/lib/setupContextUtils.js
Expand Up @@ -221,16 +221,25 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
return context.tailwindConfig.prefix + identifier
}

function resolveThemeValue(path, defaultValue, opts = {}) {
const [pathRoot, ...subPaths] = toPath(path)
const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue)
return transformThemeValue(pathRoot)(value, opts)
}

const theme = Object.assign(
(path, defaultValue = undefined) => resolveThemeValue(path, defaultValue),
{
withAlpha: (path, opacityValue) => resolveThemeValue(path, undefined, { opacityValue }),
}
)

let api = {
postcss,
prefix: applyConfiguredPrefix,
e: escapeClassName,
config: getConfigValue,
theme(path, defaultValue) {
const [pathRoot, ...subPaths] = toPath(path)
const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue)
return transformThemeValue(pathRoot)(value)
},
theme,
corePlugins: (path) => {
if (Array.isArray(tailwindConfig.corePlugins)) {
return tailwindConfig.corePlugins.includes(path)
Expand Down
84 changes: 63 additions & 21 deletions src/util/resolveConfig.js
Expand Up @@ -8,6 +8,7 @@ import { toPath } from './toPath'
import { normalizeConfig } from './normalizeConfig'
import isPlainObject from './isPlainObject'
import { cloneDeep } from './cloneDeep'
import { withAlphaValue } from './withAlphaVariable'

function isFunction(input) {
return typeof input === 'function'
Expand Down Expand Up @@ -164,40 +165,81 @@ function mergeExtensions({ extend, ...theme }) {
})
}

/**
*
* @param {string} key
* @return {Iterable<string[] & {alpha: string | undefined}>}
*/
function* toPaths(key) {
let path = toPath(key)

if (path.length === 0) {
return
}

yield path

if (Array.isArray(key)) {
return
}

let pattern = /^(.*?)\s*\/\s*([^/]+)$/
let matches = key.match(pattern)

if (matches !== null) {
let [, prefix, alpha] = matches

let newPath = toPath(prefix)
newPath.alpha = alpha

yield newPath
}
}

function resolveFunctionKeys(object) {
// theme('colors.red.500 / 0.5') -> ['colors', 'red', '500 / 0', '5]

const resolvePath = (key, defaultValue) => {
const path = toPath(key)
for (const path of toPaths(key)) {
let index = 0
let val = object

let index = 0
let val = object
while (val !== undefined && val !== null && index < path.length) {
val = val[path[index++]]

while (val !== undefined && val !== null && index < path.length) {
val = val[path[index++]]
val = isFunction(val) ? val(resolvePath, configUtils) : val
}
let shouldResolveAsFn =
isFunction(val) && (path.alpha === undefined || index < path.length - 1)

if (val === undefined) {
return defaultValue
}
val = shouldResolveAsFn ? val(resolvePath, configUtils) : val
}

if (val !== undefined) {
if (path.alpha !== undefined) {
return withAlphaValue(val, path.alpha)
}

if (isPlainObject(val)) {
return cloneDeep(val)
}

if (isPlainObject(val)) {
return cloneDeep(val)
return val
}
}

return val
return defaultValue
}

resolvePath.theme = resolvePath
// colors.red.500/50

for (let key in configUtils) {
resolvePath[key] = configUtils[key]
}
Object.assign(resolvePath, {
theme: resolvePath,
...configUtils,
})

return Object.keys(object).reduce((resolved, key) => {
return {
...resolved,
[key]: isFunction(object[key]) ? object[key](resolvePath, configUtils) : object[key],
}
resolved[key] = isFunction(object[key]) ? object[key](resolvePath, configUtils) : object[key]

return resolved
}, {})
}

Expand Down
2 changes: 1 addition & 1 deletion src/util/toPath.js
Expand Up @@ -4,7 +4,7 @@
* Square bracket notation `a[b]` may be used to "escape" dots that would otherwise be interpreted as path separators.
*
* Example:
* a -> ['a]
* a -> ['a']
* a.b.c -> ['a', 'b', 'c']
* a[b].c -> ['a', 'b', 'c']
* a[b.c].e.f -> ['a', 'b.c', 'e', 'f']
Expand Down
6 changes: 4 additions & 2 deletions src/util/transformThemeValue.js
Expand Up @@ -44,8 +44,10 @@ export default function transformThemeValue(themeSection) {
}
}

return (value) => {
if (typeof value === 'function') value = value({})
return (value, opts = {}) => {
if (typeof value === 'function') {
value = value(opts)
}

return value
}
Expand Down

0 comments on commit 50bed74

Please sign in to comment.