/** * resolve */ import { createCacheKey, getCache, setCache } from './cache'; import { NAMED_COLORS, convertRgbToHex, resolveColorFunc, resolveColorMix, resolveColorValue } from './color'; import { isString } from './common'; import { cssCalc, resolveVar } from './css-calc-var'; import { resolveRelativeColor } from './relative-color'; import { splitValue } from './util'; import { ComputedColorChannels, Options, SpecifiedColorChannels } from './typedef'; /* constants */ import { FN_COLOR, FN_MIX, SYN_COLOR_TYPE, SYN_FN_CALC, SYN_FN_LIGHT_DARK, SYN_FN_REL, SYN_FN_VAR, SYN_MIX, VAL_COMP, VAL_SPEC } from './constant'; const NAMESPACE = 'resolve'; const RGB_TRANSPARENT = 'rgba(0, 0, 0, 0)'; /* regexp */ const REG_COLOR = new RegExp(`^(?:${SYN_COLOR_TYPE})$`); const REG_FN_CALC = new RegExp(SYN_FN_CALC); const REG_FN_COLOR = /^(?:(?:ok)?l(?:ab|ch)|color(?:-mix)?|hsla?|hwb|rgba?|var)\(/; const REG_FN_LIGHT_DARK = new RegExp(SYN_FN_LIGHT_DARK); const REG_FN_REL = new RegExp(SYN_FN_REL); const REG_FN_VAR = new RegExp(SYN_FN_VAR); const REG_MIX = new RegExp(SYN_MIX); /** * resolve color * @param value - CSS color value * @param opt - options * @returns resolved color */ export const resolveColor = ( value: string, opt: Options = {} ): string | null => { if (!isString(value)) { throw new TypeError(`${value} is not a string.`); } value = value.trim(); const { colorScheme = 'normal', currentColor = '', format = VAL_COMP, nullable = false } = opt; const cacheKey: string = createCacheKey( { namespace: NAMESPACE, name: 'resolve', value }, opt ); const cachedResult = getCache(cacheKey); if (cachedResult !== false) { return cachedResult.item as string | null; } // 1. var() resolution if (REG_FN_VAR.test(value)) { if (format === VAL_SPEC) { setCache(cacheKey, value); return value; } const resolvedVar = resolveVar(value, opt); if (resolvedVar === null) { const res = format === 'hex' || format === 'hexAlpha' || nullable ? null : RGB_TRANSPARENT; setCache(cacheKey, res); return res; } value = resolvedVar; } if (opt.format !== format) { opt.format = format; } value = value.toLowerCase(); // 2. light-dark() resolution if (REG_FN_LIGHT_DARK.test(value) && value.endsWith(')')) { const colorParts = value.replace(REG_FN_LIGHT_DARK, '').replace(/\)$/, ''); const [light = '', dark = ''] = splitValue(colorParts, { delimiter: ',' }); if (light && dark) { if (format === VAL_SPEC) { const lightColor = resolveColor(light, opt); const darkColor = resolveColor(dark, opt); const res = lightColor && darkColor ? `light-dark(${lightColor}, ${darkColor})` : ''; setCache(cacheKey, res); return res; } const chosen = colorScheme === 'dark' ? dark : light; const resolved = resolveColor(chosen, opt); const res = resolved === null && !nullable ? RGB_TRANSPARENT : resolved; setCache(cacheKey, res); return res; } // fallback for invalid light-dark let invalidRes; if (format === VAL_SPEC) { invalidRes = ''; } else if (format === 'hex' || format === 'hexAlpha') { invalidRes = null; } else { invalidRes = RGB_TRANSPARENT; } setCache(cacheKey, invalidRes); return invalidRes; } // 3. Relative Color resolution if (REG_FN_REL.test(value)) { const resolvedRel = resolveRelativeColor(value, opt, resolveColor); if (format === VAL_COMP) { const res = resolvedRel === null && !nullable ? RGB_TRANSPARENT : resolvedRel; setCache(cacheKey, res); return res; } if (format === VAL_SPEC) { const res = resolvedRel === null ? '' : resolvedRel; setCache(cacheKey, res); return res; } value = resolvedRel === null ? '' : resolvedRel; } // 4. calc() resolution if (REG_FN_CALC.test(value)) { value = cssCalc(value, opt); } // 5. Keyword & Color-space resolution let cs = ''; let r = NaN; let g = NaN; let b = NaN; let alpha = NaN; if (value === 'transparent') { let res: string | null; switch (format) { case VAL_SPEC: { res = value; break; } case 'hex': { res = null; break; } case 'hexAlpha': { res = '#00000000'; break; } default: { res = RGB_TRANSPARENT; } } setCache(cacheKey, res); return res; } if (value === 'currentcolor') { if (format === VAL_SPEC) { setCache(cacheKey, value); return value; } if (currentColor) { let resolvedCurrent; if (currentColor.startsWith(FN_MIX)) { resolvedCurrent = resolveColorMix(currentColor, opt, resolveColor); } else if (currentColor.startsWith(FN_COLOR)) { resolvedCurrent = resolveColorFunc(currentColor, opt); } else { resolvedCurrent = resolveColorValue(currentColor, opt); } if (resolvedCurrent === null) { setCache(cacheKey, null); return null; } [cs, r, g, b, alpha] = resolvedCurrent as ComputedColorChannels; } else { // value is handled below if not VAL_COMP const res = format === VAL_COMP ? RGB_TRANSPARENT : value; if (format === VAL_COMP) { setCache(cacheKey, res); return res; } } } else if (format === VAL_SPEC) { let res = ''; if (value.startsWith(FN_MIX)) { const mixRes = resolveColorMix(value, opt, resolveColor); res = mixRes ? (mixRes as string) : ''; } else if (value.startsWith(FN_COLOR)) { const funcRes = resolveColorFunc(value, opt); if (isString(funcRes)) { res = funcRes; } else if (Array.isArray(funcRes)) { const [scs, rr, gg, bb, aa] = funcRes as SpecifiedColorChannels; res = aa === 1 ? `color(${scs} ${rr} ${gg} ${bb})` : `color(${scs} ${rr} ${gg} ${bb} / ${aa})`; } } else { const rgb = resolveColorValue(value, opt); if (isString(rgb)) { res = rgb; } else if (Array.isArray(rgb)) { const [scs, rr, gg, bb, aa] = rgb as SpecifiedColorChannels; if (scs === 'rgb') { res = aa === 1 ? `${scs}(${rr}, ${gg}, ${bb})` : `${scs}a(${rr}, ${gg}, ${bb}, ${aa})`; } else { res = aa === 1 ? `${scs}(${rr} ${gg} ${bb})` : `${scs}(${rr} ${gg} ${bb} / ${aa})`; } } } setCache(cacheKey, res); return res; } else if (value.startsWith(FN_MIX)) { if (currentColor) { value = value.replace(/currentcolor/g, currentColor); } value = value.replace(/transparent/g, RGB_TRANSPARENT); const resolvedMix = resolveColorMix(value, opt, resolveColor); if (resolvedMix === null) { setCache(cacheKey, null); return null; } [cs, r, g, b, alpha] = resolvedMix as ComputedColorChannels; } else if (value.startsWith(FN_COLOR)) { const resolvedFunc = resolveColorFunc(value, opt); if (resolvedFunc === null) { setCache(cacheKey, null); return null; } [cs, r, g, b, alpha] = resolvedFunc as ComputedColorChannels; } else if (value) { const resolvedVal = resolveColorValue(value, opt); if (resolvedVal === null) { setCache(cacheKey, null); return null; } [cs, r, g, b, alpha] = resolvedVal as ComputedColorChannels; } // 6. Format Finalization let finalRes: string | null = ''; switch (format) { case 'hex': case 'hexAlpha': { if ( Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b) || Number.isNaN(alpha) || (format === 'hex' && alpha === 0) ) { finalRes = null; } else { finalRes = convertRgbToHex([r, g, b, format === 'hex' ? 1 : alpha]); } break; } default: { if (cs === 'rgb') { finalRes = alpha === 1 ? `${cs}(${r}, ${g}, ${b})` : `${cs}a(${r}, ${g}, ${b}, ${alpha})`; } else if (['lab', 'lch', 'oklab', 'oklch'].includes(cs)) { finalRes = alpha === 1 ? `${cs}(${r} ${g} ${b})` : `${cs}(${r} ${g} ${b} / ${alpha})`; } else { finalRes = alpha === 1 ? `color(${cs} ${r} ${g} ${b})` : `color(${cs} ${r} ${g} ${b} / ${alpha})`; } } } setCache(cacheKey, finalRes); return finalRes; }; /** * resolve CSS color * @param value - CSS color value. system colors are not supported * @param opt - options * @returns resolved value */ export const resolve = (value: string, opt: Options = {}): string | null => { opt.nullable = false; return resolveColor(value, opt); }; /** * is color * @param value - CSS value * @param opt - options * @returns result */ export const isColor = (value: unknown, opt: Options = {}): boolean => { if (!isString(value)) { return false; } const str = value.toLowerCase().trim(); if (!str) { return false; } if (/^[a-z]+$/.test(str)) { return ( str === 'currentcolor' || str === 'transparent' || Object.hasOwn(NAMED_COLORS, str) ); } if (REG_COLOR.test(str) || REG_MIX.test(str)) { return true; } if (REG_FN_COLOR.test(str)) { const colorOpt = { ...opt, nullable: true }; if (!colorOpt.format) colorOpt.format = VAL_SPEC; return !!resolveColor(str, colorOpt); } return false; };