151 lines
3.9 KiB
JavaScript
151 lines
3.9 KiB
JavaScript
// @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: {} } }
|
|
|
|
root.each(node => {
|
|
if (node.type !== 'rule' || node.selector !== ':theme') return
|
|
|
|
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()
|
|
}
|
|
})
|
|
|
|
// 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")
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// TODO: Can this happen
|
|
if (keypath === null) {
|
|
return
|
|
}
|
|
|
|
// All other cases are config keys in a plugin
|
|
config.extend[plugin] ??= {}
|
|
|
|
if (!option) {
|
|
config.extend[plugin][keypath] = value
|
|
return
|
|
}
|
|
|
|
if (!Array.isArray(config.extend[plugin][keypath])) {
|
|
config.extend[plugin][keypath] = [
|
|
config.extend[plugin][keypath],
|
|
{}
|
|
]
|
|
}
|
|
|
|
config.extend[plugin][keypath][1][option] = value
|
|
}
|
|
|
|
/**
|
|
* 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',
|
|
}
|
|
|
|
// 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
|
|
|
|
// 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)
|
|
}
|
|
|
|
if (keypath == '' && option === 'default') {
|
|
keypath = 'DEFAULT'
|
|
option = null
|
|
} else if (keypath.startsWith('-')) {
|
|
keypath = keypath.slice(1)
|
|
}
|
|
|
|
if (keypath === '') {
|
|
return [plugin, null, null]
|
|
}
|
|
|
|
return [plugin, keypath, option === null ? null : camelize(option)]
|
|
}
|
|
|
|
return [null, null, null]
|
|
}
|
|
|
|
/**
|
|
* @param {string} str
|
|
* @returns {string}
|
|
*/
|
|
function camelize(str) {
|
|
return str.replace(/-([a-zA-Z])/g, (v) => v.slice(1).toUpperCase())
|
|
}
|