diff --git a/src/lib/parseCssConfig.js b/src/lib/parseCssConfig.js new file mode 100644 index 000000000..99a9acfd8 --- /dev/null +++ b/src/lib/parseCssConfig.js @@ -0,0 +1,102 @@ +// @ts-check + +/** + * Find and convert the `:theme` rule to a configuration object + * This is merged with the user's JS config + * It should take precedence over the JS config + * + * @param {import('postcss').Root} root + */ +export function parseCssConfig(root) { + let config = { extend: { colors: {} } } + + let theme = root.nodes.find(node => node.type === 'rule' && node.selector === ':theme') + if (!theme) return config + + theme.walkDecls((decl) => { + if (decl.prop.startsWith('--')) { + parseVariable(config, decl.prop.slice(2), decl.value) + } else { + // Only CSS variables are allowed in the theme rule + // TODO: Maybe turn this into an error? + decl.remove() + } + }) + + theme.remove() + + return config +} + +let prefixPluginMap = { + 'colors': 'colors', + 'spacing': 'spacing', + 'font-family': 'fontFamily', + 'font-size': 'fontSize', + 'shadow': 'boxShadow', +} + +/** + * Parse a key into one or more actions that modify the config and/or CSS + * + * @param {*} config + * @param {string} name + * @param {string} value + */ +function parseVariable(config, name, value) { + // Parse the variable name into a config plugin and keypath + let [plugin, keypath] = parseVariableName(name, value) + + console.log({ name, plugin, keypath, value }) + + // We couldn't find a plugin for this variable, so we can't do anything with it + if (plugin === null) return + + if (keypath === null && value === 'unset') { + // when using `--some-plugin: unset` we want to reset whatever the default value is + config[plugin] = {} + + // This also means there is no reason to keep the variable in the CSS + decl.remove() + + return + } + + // When using `--some-plugin: initial` it should reset to the default value for that key + // This effectively means removing it + if (keypath === null && value === 'initial') { + delete config[plugin] + delete config.extend[plugin] + + // This also means there is no reason to keep the variable in the CSS + decl.remove() + + return + } + + // All other cases are config keys in a plugin + config.extend[plugin] ??= {} + config.extend[plugin][keypath] = value +} + +/** + * Parse a key into one or more actions that modify the config and/or CSS + * + * @param {string} name + * @param {string} value + * @returns {[plugin: string|null, keypath: string|null]} + */ +function parseVariableName(name, value) { + for (const [prefix, plugin] of Object.entries(prefixPluginMap)) { + if (!name.startsWith(prefix)) continue + + // The keypath is the part of the variable name after the prefix + let keypath = name.slice(prefix.length) + if (keypath.startsWith('-')) keypath = keypath.slice(1) + if (keypath === '') keypath = null + + return [plugin, keypath] + } + + return [null, null] +} diff --git a/src/plugin.js b/src/plugin.js index d617b55cf..396f9b54c 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -6,6 +6,7 @@ import processTailwindFeatures from './processTailwindFeatures' import { env } from './lib/sharedState' import { findAtConfigPath } from './lib/findAtConfigPath' import { handleImportAtRules } from './lib/handleImportAtRules' +import { parseCssConfig } from './lib/parseCssConfig' module.exports = function tailwindcss(configOrPath) { return { @@ -23,24 +24,8 @@ module.exports = function tailwindcss(configOrPath) { // path for the file being processed configOrPath = findAtConfigPath(root, result) ?? configOrPath - // Begin CSS-first theme configuration proof-of-concept - let themeValues = { extend: { colors: {} } } - - root.each((node) => { - if (node.type === 'rule' && node.selector === ':theme') { - // Walk :theme pseudo and extract values - node.walkDecls((decl) => { - if (decl.prop.startsWith('--colors-')) { - themeValues.extend.colors[decl.prop.slice(9)] = decl.value - } - }) - - node.remove() - } - }) - - // context.tailwindConfig.theme - // End CSS-first theme configuration proof-of-concept + // Parse any CSS-based config values from `:theme` rules + let themeValues = parseCssConfig(root) let context = setupTrackingContext(configOrPath, themeValues)