import { BindingMetadata } from './types' import { SFCDescriptor } from './parseComponent' import { PluginCreator } from 'postcss' import hash from 'hash-sum' import { prefixIdentifiers } from './prefixIdentifiers' export const CSS_VARS_HELPER = `useCssVars` export function genCssVarsFromList( vars: string[], id: string, isProd: boolean, isSSR = false ): string { return `{\n ${vars .map( key => `"${isSSR ? `--` : ``}${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 normalizeExpression(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 } const vBindRE = /v-bind\s*\(/g 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 = vBindRE.exec(content))) { const start = match.index + match[0].length const end = lexBinding(content, start) if (end !== null) { const variable = normalizeExpression(content.slice(start, end)) if (!vars.includes(variable)) { vars.push(variable) } } } }) return vars } const enum LexerState { inParens, inSingleQuoteString, inDoubleQuoteString } function lexBinding(content: string, start: number): number | null { let state: LexerState = LexerState.inParens let parenDepth = 0 for (let i = start; i < content.length; i++) { const char = content.charAt(i) switch (state) { case LexerState.inParens: if (char === `'`) { state = LexerState.inSingleQuoteString } else if (char === `"`) { state = LexerState.inDoubleQuoteString } else if (char === `(`) { parenDepth++ } else if (char === `)`) { if (parenDepth > 0) { parenDepth-- } else { return i } } break case LexerState.inSingleQuoteString: if (char === `'`) { state = LexerState.inParens } break case LexerState.inDoubleQuoteString: if (char === `"`) { state = LexerState.inParens } break } } return null } // 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 const value = decl.value if (vBindRE.test(value)) { vBindRE.lastIndex = 0 let transformed = '' let lastIndex = 0 let match while ((match = vBindRE.exec(value))) { const start = match.index + match[0].length const end = lexBinding(value, start) if (end !== null) { const variable = normalizeExpression(value.slice(start, end)) transformed += value.slice(lastIndex, match.index) + `var(--${genVarName(id, variable, isProd)})` lastIndex = end + 1 } } decl.value = transformed + value.slice(lastIndex) } } } } cssVarsPlugin.postcss = true export function genCssVarsCode( vars: string[], bindings: BindingMetadata, id: string, isProd: boolean ) { const varsExp = genCssVarsFromList(vars, id, isProd) return `_${CSS_VARS_HELPER}((_vm, _setup) => ${prefixIdentifiers( `(${varsExp})`, false, false, undefined, bindings )})` } //