diff --git a/packages/compiler-sfc/__tests__/cssVars.spec.ts b/packages/compiler-sfc/__tests__/cssVars.spec.ts index d9912f44b51..9d3df069e76 100644 --- a/packages/compiler-sfc/__tests__/cssVars.spec.ts +++ b/packages/compiler-sfc/__tests__/cssVars.spec.ts @@ -1,4 +1,4 @@ -import { compileStyle } from '../src' +import { compileStyle, parse } from '../src' import { mockId, compileSFCScript, assertCode } from './utils' describe('CSS vars injection', () => { @@ -231,5 +231,21 @@ describe('CSS vars injection', () => { })`) assertCode(content) }) + + // #6022 + test('should be able to parse incomplete expressions', () => { + const { + descriptor: { cssVars } + } = parse( + ` + ` + ) + expect(cssVars).toMatchObject([`count.toString(`, `xxx`]) + }) }) }) diff --git a/packages/compiler-sfc/src/cssVars.ts b/packages/compiler-sfc/src/cssVars.ts index a922e2de7cf..10f9bb480f1 100644 --- a/packages/compiler-sfc/src/cssVars.ts +++ b/packages/compiler-sfc/src/cssVars.ts @@ -12,8 +12,6 @@ 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[], @@ -47,22 +45,71 @@ function normalizeExpression(exp: string) { 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 = cssVarRE.exec(content))) { - const variable = normalizeExpression(match[1]) - if (!vars.includes(variable)) { - vars.push(variable) + 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 @@ -75,10 +122,24 @@ export const cssVarsPlugin: PluginCreator = opts => { 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, normalizeExpression($1), isProd)})` - }) + 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) } } }