This commit is contained in:
Jordan Pittman 2023-06-14 14:03:07 -04:00 • committed by Robin Malfait
parent 7573cc7d2a
commit 67fc3c30e5
No known key found for this signature in database
GPG key ID: 92F53D68B9041AFE
2 changed files with 166 additions and 133 deletions

View file

@ -1,160 +1,193 @@
// @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: {} } }
// config[plugin] = null // Remomve from base config
// config[plugin][key] = null // not supported
// config[plugin] = "anything" // not supported
// config[plugin][key] = "foo" // not supported
root.each(node => {
if (node.type !== 'rule' || node.selector !== ':theme') return
export class CssBasedConfig {
/** @type {*} */
config = {};
node.walkDecls((decl) => {
if (decl.prop.startsWith('--')) {
parseVariable(config, decl)
} else {
// Only CSS variables are allowed in the theme rule
// TODO: Maybe turn this into an error?
decl.remove()
/**
* 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
* @returns {*}
*/
parse(root) {
this.config = { extend: {} }
let rules = this.getThemeRules(root)
rules.forEach(rule => {
rule.walkDecls((decl) => {
if (decl.prop.startsWith('--')) {
this.parseVariable(decl)
} else {
throw decl.error('Only CSS variables are allowed in `:theme`')
}
})
// Allow the variables to be used with var() anywhere in CSS
rule.selector = ':root'
})
return this.config
}
/**
* Walk the tree and find all `:theme` rules
*
* @param {import('postcss').Root} root
* @returns {import('postcss').Rule[]}
*/
getThemeRules(root) {
let rules = []
root.each(node => {
if (node.type === 'rule' && node.selector === ':theme') {
rules.push(node)
}
})
// Allow the variables to be used with var() anywhere in CSS
node.selector = ':root'
})
return config
}
/**
* Parse a key into one or more actions that modify the config and/or CSS
*
* @param {*} config
* @param {import('postcss').Declaration} decl
*/
function parseVariable(config, decl) {
// Parse the variable name into a config plugin and keypath
let [name, value] = [decl.prop.slice(2), decl.value]
let [plugin, keypath, option] = parseVariableName(name)
// 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
} else if (keypath !== null && value === 'unset') {
// when using `--plugin-key: unset` we want to remove that from the config
throw new Error("Not implemented yet")
return rules
}
// 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]
/**
* Parse a variable declaration and modify the config
*
* @param {import('postcss').Declaration} decl
*/
parseVariable(decl) {
let config = this.config
// This also means there is no reason to keep the variable in the CSS
decl.remove()
// Parse the variable name into a config plugin and keypath
let [name, value] = [decl.prop.slice(2), decl.value]
let [plugin, keypath, option] = this.parseVariableName(name)
return
}
// We couldn't find a plugin for this variable, so we can't do anything with it
if (plugin === null) return
// TODO: Can this happen
if (keypath === null) {
return
}
if (keypath === null && value === 'unset') {
// when using `--some-plugin: unset` we want to reset whatever the default value is
config[plugin] = {}
// All other cases are config keys in a plugin
config.extend[plugin] ??= {}
// This also means there is no reason to keep the variable in the CSS
decl.remove()
let [path, key] = [
keypath.slice(0, -1),
keypath[keypath.length-1],
]
let obj = config.extend[plugin]
let segment
while (segment = path.shift()) {
obj[segment] ??= {}
obj = obj[segment]
}
if (option) {
// TODO: This is weird
if (!Array.isArray(obj[key])) {
obj[key] = [obj[key], {}]
return
} else if (keypath !== null && value === 'unset') {
// when using `--plugin-key: unset` we want to remove that from the config
throw new Error("Not implemented yet")
}
obj[key][1][option] = value
} else {
obj[key] = value
}
}
// 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]
/**
* Parse a key into one or more actions that modify the config and/or CSS
*
* @param {string} name
* @returns {[plugin: string|null, keypath: string[]|null, option: string|null]}
*/
function parseVariableName(name) {
const prefixPluginMap = {
'colors': 'colors',
'spacing': 'spacing',
'font-family': 'fontFamily',
'font-size': 'fontSize',
'shadow': 'boxShadow',
'blur': 'blur',
// This also means there is no reason to keep the variable in the CSS
decl.remove()
return
}
// TODO: Can this happen
if (keypath === null) {
return
}
// All other cases are config keys in a plugin
config.extend[plugin] ??= {}
let [path, key] = [
keypath.slice(0, -1),
keypath[keypath.length-1],
]
let obj = config.extend[plugin]
let segment
while (segment = path.shift()) {
obj[segment] ??= {}
obj = obj[segment]
}
if (option) {
// TODO: This is weird
if (!Array.isArray(obj[key])) {
obj[key] = [obj[key], {}]
}
obj[key][1][option] = value
} else {
obj[key] = value
}
}
// Unescape the name if needed
// This is needed for CSS variables that contain special characters
name = name.replace(/\\/g, '')
/**
* Convert a variable name into:
* - The associated plugin
* - A keypath to modify inside plugin's config (if available)
* - An "option" which is a key into the "options" object when a Tuple-like value is used
*
* @param {string} name
* @returns {[plugin: string|null, keypath: string[]|null, option: string|null]}
*/
parseVariableName(name) {
// Unescape the name if needed
// This is needed for CSS variables that contain special characters
name = name.replace(/\\/g, '')
for (const [prefix, plugin] of Object.entries(prefixPluginMap)) {
if (!name.startsWith(prefix)) continue
for (const [prefix, plugin] of Object.entries(this.prefixPluginMap)) {
if (!name.startsWith(prefix)) continue
// The keypath is the part of the variable name after the prefix
let keypath = name.slice(prefix.length)
let option = null
// The keypath is the part of the variable name after the prefix
let keypath = name.slice(prefix.length)
let option = null
let optionStart = keypath.indexOf('--')
if (optionStart !== -1) {
option = keypath.slice(optionStart+2)
keypath = keypath.slice(0, optionStart)
let optionStart = keypath.indexOf('--')
if (optionStart !== -1) {
option = keypath.slice(optionStart+2)
keypath = keypath.slice(0, optionStart)
}
if (keypath == '' && option === 'default') {
keypath = 'DEFAULT'
option = null
} else if (keypath.startsWith('-')) {
keypath = keypath.slice(1)
}
if (keypath === '') {
return [plugin, null, null]
}
option = option === null ? null : camelize(option);
if (plugin === 'colors') {
return [plugin, keypath.split('-'), option]
}
return [plugin, [keypath], option]
}
if (keypath == '' && option === 'default') {
keypath = 'DEFAULT'
option = null
} else if (keypath.startsWith('-')) {
keypath = keypath.slice(1)
}
if (keypath === '') {
return [plugin, null, null]
}
option = option === null ? null : camelize(option);
if (plugin === 'colors') {
return [plugin, keypath.split('-'), option]
}
return [plugin, [keypath], option]
return [null, null, null]
}
return [null, null, null]
get prefixPluginMap() {
return {
'colors': 'colors',
'spacing': 'spacing',
'font-family': 'fontFamily',
'font-size': 'fontSize',
'shadow': 'boxShadow',
'blur': 'blur',
}
}
}
/**

View file

@ -6,7 +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'
import { CssBasedConfig } from './lib/parseCssConfig'
module.exports = function tailwindcss(configOrPath) {
return {
@ -25,7 +25,7 @@ module.exports = function tailwindcss(configOrPath) {
configOrPath = findAtConfigPath(root, result) ?? configOrPath
// Parse any CSS-based config values from `:theme` rules
let themeValues = parseCssConfig(root)
let themeValues = new CssBasedConfig().parse(root)
let context = setupTrackingContext(configOrPath, themeValues)