]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-sfc): improve css v-bind parsing
authorEvan You <yyx990803@gmail.com>
Mon, 6 Jun 2022 12:02:08 +0000 (20:02 +0800)
committerEvan You <yyx990803@gmail.com>
Mon, 6 Jun 2022 12:02:08 +0000 (20:02 +0800)
fix #6022

packages/compiler-sfc/__tests__/cssVars.spec.ts
packages/compiler-sfc/src/cssVars.ts

index d9912f44b514f5d6528a5edfb83dcac6884350e6..9d3df069e76f217439628e07ed0c21c0c8992626 100644 (file)
@@ -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(
+        `<script setup>let xxx = 1</script>
+        <style scoped>
+        label {
+          font-weight: v-bind("count.toString(");
+          font-weight: v-bind(xxx);
+        }
+        </style>`
+      )
+      expect(cssVars).toMatchObject([`count.toString(`, `xxx`])
+    })
   })
 })
index a922e2de7cf24de1605b7088610dd9bc95441803..10f9bb480f1ab78bf0404a6f1e7e89a8f5a92943 100644 (file)
@@ -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<CssVarsPluginOptions> = 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)
       }
     }
   }