tailwind-theme-preset
Version:
A Tailwind CSS plugin for unified theme management with automatic CSS variable generation, supporting multi-theme configurations and Shadcn/ui compatibility.
211 lines (209 loc) • 8.99 kB
JavaScript
;
//#region src/index.ts
let flattenedTheme = {};
function presetTheme(theme, options) {
const mergedTheme = deepMerge({}, theme);
flattenedTheme = flatten(mergedTheme);
return {
safelist: ["dark", ...options.safelist || []],
theme: { extend: { colors: generateColors(mergedTheme, options) } },
plugins: [processPlugin(mergedTheme)]
};
}
function generateColors(theme, options = { colorRule: "hsl" }) {
const colors = {};
for (const key in theme) {
const value = theme[key];
if (typeof value === "object") for (const cssVarKey in value) {
const colorKey = `${key}-${cssVarKey}`;
const colorValue = value[cssVarKey];
if (typeof colorValue === "object" && !Array.isArray(colorValue)) {
const processedValue = deepMerge({}, colorValue);
colors[colorKey] = processedColor(processedValue, `--${colorKey}`);
} else if (cssVarKey === "DEFAULT") colors[key] = { DEFAULT: `var(--${key}, ${colorValue})` };
else {
colors[key] = colors[key] || {};
colors[key][cssVarKey] = colors[key][cssVarKey] || {};
if (typeof colorValue === "string") colors[key][cssVarKey] = { DEFAULT: colorValue };
else if (Array.isArray(colorValue)) {
if (colorValue[1] === void 0) colors[key][cssVarKey] = `var(--${colorKey}, ${colorValue[0]})`;
else if (typeof colorValue[1] === "string") colors[key][cssVarKey] = `${colorValue[1]}(var(--${colorKey}, ${colorValue[0]}))`;
else if (typeof colorValue[1] === "function") colors[key][cssVarKey] = colorValue[1](`--${colorKey}`, colorValue[0]);
}
}
}
}
console.log("[THEME] Extend colors:", colors);
return colors;
function processedColor(colorValue, prefixKey) {
for (const key in colorValue) {
const value = colorValue[key];
if (typeof value === "string" || Array.isArray(value)) {
let colorString;
let colorRule = options.colorRule || "hsl";
let customHandler;
if (Array.isArray(value)) {
colorString = value[0];
const secondParam = value.length > 1 ? value[1] : void 0;
if (typeof secondParam === "string") colorRule = secondParam;
else if (typeof secondParam === "function") customHandler = secondParam;
else if (secondParam === void 0) {
colorValue[key] = `var(${prefixKey}${key === "DEFAULT" ? "" : `-${key}`}, ${colorString})`;
continue;
}
} else if (typeof value === "string") colorString = value;
else continue;
const currentPrefixKey = key === "DEFAULT" ? prefixKey : `${prefixKey}-${key}`;
if (customHandler) {
colorValue[key] = customHandler(currentPrefixKey, colorString);
continue;
}
const isCompleteColor = /^(?:rgb|rgba|hsl|hsla)\s*\(/.test(colorString) || /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(colorString) || /^var\s*\(/.test(colorString);
if (isCompleteColor) colorValue[key] = `var(${currentPrefixKey}, ${colorString})`;
else colorValue[key] = `${colorRule}(var(${currentPrefixKey}, ${colorString}))`;
} else if (typeof value === "object" && value !== null) processedColor(value, `${prefixKey}-${key}`);
}
return colorValue;
}
}
function processTheme(theme) {
const processed = { ":root": {} };
for (const key in theme) {
const value = theme[key];
if (typeof value === "object") for (const cssVarKey in value) {
const cssVarValue = value[cssVarKey];
const processCssVarKey = `--${key}-${cssVarKey}`;
if (typeof cssVarValue === "object" && !Array.isArray(cssVarValue)) processedTheme(cssVarValue, processCssVarKey, processed);
else if (cssVarKey === "DEFAULT") {
if (typeof cssVarValue === "string") processed[":root"][`--${key}`] = cssVarValue;
else if (Array.isArray(cssVarValue)) processed[":root"][`--${key}`] = cssVarValue[0];
} else if (typeof cssVarValue === "string") {
processed[`.${cssVarKey}`] = processed[`.${cssVarKey}`] || {};
processed[`.${cssVarKey}`][`--${key}`] = cssVarValue;
} else if (Array.isArray(cssVarValue)) processed[":root"][processCssVarKey] = cssVarValue[0];
}
}
return processed;
}
function processedTheme(colorValue, prefixKey, processed) {
for (const key in colorValue) {
const value = colorValue[key];
if (typeof value === "object" && value !== null && !Array.isArray(value)) processedTheme(value, `${prefixKey}-${key}`, processed);
else if (key === "DEFAULT") {
if (typeof value === "string") if (/(?:hsl|rgb)\(var\(/.test(value)) processed[":root"][prefixKey] = resolvedValue(value);
else processed[":root"][prefixKey] = value;
else if (Array.isArray(value)) if (/(?:hsl|rgb)\(var\(/.test(value[0])) processed[":root"][prefixKey] = resolvedValue(value[0]);
else processed[":root"][prefixKey] = value[0];
} else if (typeof value === "string") {
processed[`.${key}`] = processed[`.${key}`] || {};
if (/(?:hsl|rgb)\(var\(/.test(value)) processed[`.${key}`][prefixKey] = resolvedValue(value);
else processed[`.${key}`][prefixKey] = value;
}
}
}
function processPlugin(themeConfig) {
const processedTheme$1 = processTheme(themeConfig);
console.log("[PLUGINS] addUtilities:", processedTheme$1);
return ({ addUtilities }) => {
addUtilities(processedTheme$1);
};
}
function deepMerge(...args) {
if (args.length < 2) return args[0];
const target = args[0] || {};
for (let i = 1; i < args.length; i++) {
const source = args[i];
if (!source) continue;
for (const key in source) if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) target[key] = deepMerge(target[key] || {}, source[key]);
else target[key] = source[key];
}
return target;
}
function resolvedValue(cssVarValue) {
return resolveNestedColorFunctions(cssVarValue, [], new Set());
}
/**
* 递归解析嵌套的颜色函数和变量引用
* @param value 要处理的CSS值
* @param visited 已访问的变量路径数组,用于生成注释
* @param visitedSet 已访问的变量集合,防止循环引用
*/
function resolveNestedColorFunctions(value, visited, visitedSet) {
const colorFuncRegex = /(hsl|rgb|rgba|hsla)\s*\((.*)\)/g;
return value.replace(colorFuncRegex, (match, funcName, content) => {
const { resolvedContent, finalVisited } = resolveVariablesInContent(content, visited, visitedSet);
const nestedFuncRegex = new RegExp(`(${funcName})\\s*\\(([^)]+)\\)`);
const nestedMatch = resolvedContent.match(nestedFuncRegex);
if (nestedMatch) {
const innerContent = nestedMatch[2];
const remainingContent = resolvedContent.replace(nestedFuncRegex, innerContent);
if (finalVisited.length > 0) {
const originalVar = `var(${finalVisited[0]})`;
const finalVar = `var(${finalVisited.slice(-1)[0]})`;
const result$1 = match.replace(originalVar, finalVar);
return `${result$1}/* ${finalVisited.join(" -> ")} */`;
}
return `${funcName}(${remainingContent})`;
}
const result = `${funcName}(${resolvedContent})`;
if (finalVisited.length > 0) {
const originalVar = `var(${finalVisited[0]})`;
const finalVar = `var(${finalVisited.slice(-1)[0]})`;
const resultWithReplacedVar = match.replace(originalVar, finalVar);
return `${resultWithReplacedVar}/* ${finalVisited.join(" -> ")} */`;
}
return result;
});
}
/**
* 解析内容中的变量引用
* @param content 要解析的内容
* @param visited 已访问的变量路径数组
* @param visitedSet 已访问的变量集合
* @returns 解析后的内容和更新的访问路径
*/
function resolveVariablesInContent(content, visited, visitedSet) {
const varRegex = /var\s*\(\s*(--[^,)]+)(?:\s*,\s*([^)]+))?\s*\)/g;
let finalVisited = [...visited];
const resolvedContent = content.replace(varRegex, (match, varName) => {
if (visitedSet.has(varName)) return match;
const varValue = flattenedTheme[varName];
if (!varValue) return match;
const newVisitedSet = new Set(visitedSet);
newVisitedSet.add(varName);
const newVisited = [...visited, varName];
const colorFuncMatch = varValue.match(/^(hsl|rgb|rgba|hsla)\s*\((.+)\)$/);
if (colorFuncMatch) {
const [, _funcName, innerContent] = colorFuncMatch;
const { resolvedContent: resolvedInnerContent, finalVisited: innerFinalVisited } = resolveVariablesInContent(innerContent, newVisited, newVisitedSet);
finalVisited = innerFinalVisited;
return resolvedInnerContent;
} else {
const resolvedVarValue = resolveNestedColorFunctions(varValue, newVisited, newVisitedSet);
finalVisited = newVisited;
return resolvedVarValue;
}
});
return {
resolvedContent,
finalVisited
};
}
function flatten(theme) {
const result = {};
function recurse(curr, prefix) {
for (const key in curr) {
const value = curr[key];
const newKey = prefix ? key === "DEFAULT" ? prefix : `${prefix}-${key}` : `--${key}`;
if (typeof value === "object" && value !== null && !Array.isArray(value)) recurse(value, newKey);
else result[newKey] = value;
}
}
recurse(theme, "");
return result;
}
//#endregion
exports.flatten = flatten
exports.generateColors = generateColors
exports.presetTheme = presetTheme
exports.processTheme = processTheme