import { processExpression, createTransformContext, createSimpleExpression, createRoot, NodeTypes, SimpleExpressionNode, BindingMetadata } from '@vue/compiler-dom' import { SFCDescriptor } from './parse' import { PluginCreator } from 'postcss' import hash from 'hash-sum' export const CSS_VARS_HELPER = `useCssVars` // match v-bind() with max 2-levels of nested parens. const cssVarRE = /v-bind\s*\(((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*)\)/g export function genCssVarsFromList( vars: string[], id: string, isProd: boolean ): string { return `{\n ${vars .map(key => `"${genVarName(id, key, isProd)}": (${key})`) .join(',\n ')}\n}` } function genVarName(id: string, raw: string, isProd: boolean): string { if (isProd) { return hash(id + raw) } else { return `${id}-${raw.replace(/([^\w-])/g, '_')}` } } function noramlizeExpression(exp: string) { exp = exp.trim() if ( (exp[0] === `'` && exp[exp.length - 1] === `'`) || (exp[0] === `"` && exp[exp.length - 1] === `"`) ) { return exp.slice(1, -1) } return exp } export function parseCssVars(sfc: SFCDescriptor): string[] { const vars: string[] = [] sfc.styles.forEach(style => { let match // ignore v-bind() in comments /* ... */ const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '') while ((match = cssVarRE.exec(content))) { const variable = noramlizeExpression(match[1]) if (!vars.includes(variable)) { vars.push(variable) } } }) return vars } // for compileStyle export interface CssVarsPluginOptions { id: string isProd: boolean } export const cssVarsPlugin: PluginCreator = opts => { const { id, isProd } = opts! return { postcssPlugin: 'vue-sfc-vars', Declaration(decl) { // rewrite CSS variables if (cssVarRE.test(decl.value)) { decl.value = decl.value.replace(cssVarRE, (_, $1) => { return `var(--${genVarName(id, noramlizeExpression($1), isProd)})` }) } } } } cssVarsPlugin.postcss = true export function genCssVarsCode( vars: string[], bindings: BindingMetadata, id: string, isProd: boolean ) { const varsExp = genCssVarsFromList(vars, id, isProd) const exp = createSimpleExpression(varsExp, false) const context = createTransformContext(createRoot([]), { prefixIdentifiers: true, inline: true, bindingMetadata: bindings.__isScriptSetup === false ? undefined : bindings }) const transformed = processExpression(exp, context) const transformedString = transformed.type === NodeTypes.SIMPLE_EXPRESSION ? transformed.content : transformed.children .map(c => { return typeof c === 'string' ? c : (c as SimpleExpressionNode).content }) .join('') return `_${CSS_VARS_HELPER}(_ctx => (${transformedString}))` } //