import type { Config as TwConfig } from 'tailwindcss' import defaultColors from 'tailwindcss/colors.js' import { camelCase, upperFirst } from 'scule' import { omit } from './objectUtils' const colorsToRegex = (colors: string[]): string => colors.join('|') type ColorConfig = Exclude['colors'], Function> export const excludeColors = ( colors: ColorConfig | typeof defaultColors, ): string[] => { const colorEntries = Object.entries(omit(colors as Record, [])) return colorEntries .filter(([, value]) => typeof value === 'object') .map(([key]) => key) } export const setColors = (theme: TwConfig['theme']) => { const _globalColors: ColorConfig = { ...(theme?.colors || defaultColors), ...theme?.extend?.colors, } // @ts-ignore _globalColors.primary = theme.extend.colors.primary = { 50: 'rgb(var(--color-primary-50) / )', 100: 'rgb(var(--color-primary-100) / )', 200: 'rgb(var(--color-primary-200) / )', 300: 'rgb(var(--color-primary-300) / )', 400: 'rgb(var(--color-primary-400) / )', 500: 'rgb(var(--color-primary-500) / )', 600: 'rgb(var(--color-primary-600) / )', 700: 'rgb(var(--color-primary-700) / )', 800: 'rgb(var(--color-primary-800) / )', 900: 'rgb(var(--color-primary-900) / )', 950: 'rgb(var(--color-primary-950) / )', DEFAULT: 'rgb(var(--color-primary-DEFAULT) / )', } if (_globalColors.gray) { // @ts-ignore _globalColors.cool = theme.extend.colors.cool = defaultColors.gray } // @ts-ignore _globalColors.gray = theme.extend.colors.gray = { 50: 'rgb(var(--color-gray-50) / )', 100: 'rgb(var(--color-gray-100) / )', 200: 'rgb(var(--color-gray-200) / )', 300: 'rgb(var(--color-gray-300) / )', 400: 'rgb(var(--color-gray-400) / )', 500: 'rgb(var(--color-gray-500) / )', 600: 'rgb(var(--color-gray-600) / )', 700: 'rgb(var(--color-gray-700) / )', 800: 'rgb(var(--color-gray-800) / )', 900: 'rgb(var(--color-gray-900) / )', 950: 'rgb(var(--color-gray-950) / )', } return excludeColors(_globalColors) } const safelistForComponent: Record< string, (colors: string) => TwConfig['safelist'] > = { button: colorsToRegex => [ { pattern: RegExp(`^bg-(${colorsToRegex})-50$`), variants: ['hover', 'disabled'], }, { pattern: RegExp(`^bg-(${colorsToRegex})-100$`), variants: ['hover'], }, { pattern: RegExp(`^bg-(${colorsToRegex})-400$`), variants: ['dark', 'dark:disabled'], }, { pattern: RegExp(`^bg-(${colorsToRegex})-500$`), variants: ['disabled', 'dark:hover', 'dark:active'], }, { pattern: RegExp(`^bg-(${colorsToRegex})-600$`), variants: ['hover'], }, { pattern: RegExp(`^bg-(${colorsToRegex})-700$`), variants: ['active'], }, { pattern: RegExp(`^bg-(${colorsToRegex})-900$`), variants: ['dark:hover'], }, { pattern: RegExp(`^bg-(${colorsToRegex})-950$`), variants: ['dark', 'dark:hover', 'dark:disabled'], }, { pattern: RegExp(`^text-(${colorsToRegex})-400$`), variants: ['dark', 'dark:hover', 'dark:disabled'], }, { pattern: RegExp(`^text-(${colorsToRegex})-500$`), variants: ['dark:hover', 'disabled'], }, { pattern: RegExp(`^text-(${colorsToRegex})-600$`), variants: ['hover'], }, { pattern: RegExp(`^outline-(${colorsToRegex})-400$`), variants: ['dark:focus-visible'], }, { pattern: RegExp(`^outline-(${colorsToRegex})-500$`), variants: ['focus-visible'], }, { pattern: RegExp(`^ring-(${colorsToRegex})-300$`), variants: ['focus', 'dark:focus'], }, { pattern: RegExp(`^ring-(${colorsToRegex})-400$`), variants: ['dark:focus-visible'], }, { pattern: RegExp(`^ring-(${colorsToRegex})-500$`), variants: ['focus-visible'], }, ], message: colorsToRegex => [ { pattern: RegExp(`^bg-(${colorsToRegex})-50$`), }, { pattern: RegExp(`^bg-(${colorsToRegex})-900$`), variants: ['dark'], }, { pattern: RegExp(`^text-(${colorsToRegex})-500$`), }, { pattern: RegExp(`^text-(${colorsToRegex})-300$`), variants: ['dark'], }, { pattern: RegExp(`^border-(${colorsToRegex})-400$`), }, { pattern: RegExp(`^border-(${colorsToRegex})-600$`), variants: ['dark'], }, ], } export const generateSafelist = (colors: string[], globalColors: string[]) => { const safelist = Object.keys(safelistForComponent).flatMap(component => safelistForComponent[component](colorsToRegex(colors)), ) return [...safelist] } type SafelistFn = Exclude< NonNullable['extract']>, Record > export const customSafelistExtractor = ( prefix: string, content: string, colors: string[], safelistColors: string[], ): ReturnType => { const classes: string[] = [] const regex = /<([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z][A-Za-z0-9]*)*)\s+(?![^>]*:color\b)[^>]*\bcolor=["']([^"']+)["'][^>]*>/g const matches = content.matchAll(regex) const components = Object.keys(safelistForComponent).map( component => `${prefix}${component.charAt(0).toUpperCase() + component.slice(1)}`, ) for (const match of matches) { const [, component, color] = match const camelComponent = upperFirst(camelCase(component)) if (!colors.includes(color) || safelistColors.includes(color)) { continue } let name = camelComponent as string if (!components.includes(name)) { continue } name = name.replace(prefix, '').toLowerCase() const matchClasses = safelistForComponent[name](color)?.flatMap((group) => { return typeof group === 'string' ? '' : ['', ...(group.variants || [])].flatMap((variant) => { const matches = group.pattern.source.match(/\(([^)]+)\)/g) return ( matches ?.map((match) => { const colorOptions = match .substring(1, match.length - 1) .split('|') return colorOptions.map((color) => { const classesExtracted = group.pattern.source .replace(match, color) .replace('^', '') .replace('$', '') if (variant) { return `${variant}:${classesExtracted}` } return classesExtracted }) }) .flat() || [] ) }) }) classes.push(...(matchClasses as string[])) } return classes }