Skip to content

Commit e60244b

Browse files
committed
fix(compiler-sfc): improve css v-bind parsing
fix #6022
1 parent 9734b31 commit e60244b

File tree

2 files changed

+88
-11
lines changed

2 files changed

+88
-11
lines changed

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

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { compileStyle } from '../src'
1+
import { compileStyle, parse } from '../src'
22
import { mockId, compileSFCScript, assertCode } from './utils'
33

44
describe('CSS vars injection', () => {
@@ -231,5 +231,21 @@ describe('CSS vars injection', () => {
231231
})`)
232232
assertCode(content)
233233
})
234+
235+
// #6022
236+
test('should be able to parse incomplete expressions', () => {
237+
const {
238+
descriptor: { cssVars }
239+
} = parse(
240+
`<script setup>let xxx = 1</script>
241+
<style scoped>
242+
label {
243+
font-weight: v-bind("count.toString(");
244+
font-weight: v-bind(xxx);
245+
}
246+
</style>`
247+
)
248+
expect(cssVars).toMatchObject([`count.toString(`, `xxx`])
249+
})
234250
})
235251
})

packages/compiler-sfc/src/cssVars.ts

+71-10
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import { PluginCreator } from 'postcss'
1212
import hash from 'hash-sum'
1313

1414
export const CSS_VARS_HELPER = `useCssVars`
15-
// match v-bind() with max 2-levels of nested parens.
16-
const cssVarRE = /v-bind\s*\(((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*)\)/g
1715

1816
export function genCssVarsFromList(
1917
vars: string[],
@@ -47,22 +45,71 @@ function normalizeExpression(exp: string) {
4745
return exp
4846
}
4947

48+
const vBindRE = /v-bind\s*\(/g
49+
5050
export function parseCssVars(sfc: SFCDescriptor): string[] {
5151
const vars: string[] = []
5252
sfc.styles.forEach(style => {
5353
let match
5454
// ignore v-bind() in comments /* ... */
5555
const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '')
56-
while ((match = cssVarRE.exec(content))) {
57-
const variable = normalizeExpression(match[1])
58-
if (!vars.includes(variable)) {
59-
vars.push(variable)
56+
while ((match = vBindRE.exec(content))) {
57+
const start = match.index + match[0].length
58+
const end = lexBinding(content, start)
59+
if (end !== null) {
60+
const variable = normalizeExpression(content.slice(start, end))
61+
if (!vars.includes(variable)) {
62+
vars.push(variable)
63+
}
6064
}
6165
}
6266
})
6367
return vars
6468
}
6569

70+
const enum LexerState {
71+
inParens,
72+
inSingleQuoteString,
73+
inDoubleQuoteString
74+
}
75+
76+
function lexBinding(content: string, start: number): number | null {
77+
let state: LexerState = LexerState.inParens
78+
let parenDepth = 0
79+
80+
for (let i = start; i < content.length; i++) {
81+
const char = content.charAt(i)
82+
switch (state) {
83+
case LexerState.inParens:
84+
if (char === `'`) {
85+
state = LexerState.inSingleQuoteString
86+
} else if (char === `"`) {
87+
state = LexerState.inDoubleQuoteString
88+
} else if (char === `(`) {
89+
parenDepth++
90+
} else if (char === `)`) {
91+
if (parenDepth > 0) {
92+
parenDepth--
93+
} else {
94+
return i
95+
}
96+
}
97+
break
98+
case LexerState.inSingleQuoteString:
99+
if (char === `'`) {
100+
state = LexerState.inParens
101+
}
102+
break
103+
case LexerState.inDoubleQuoteString:
104+
if (char === `"`) {
105+
state = LexerState.inParens
106+
}
107+
break
108+
}
109+
}
110+
return null
111+
}
112+
66113
// for compileStyle
67114
export interface CssVarsPluginOptions {
68115
id: string
@@ -75,10 +122,24 @@ export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
75122
postcssPlugin: 'vue-sfc-vars',
76123
Declaration(decl) {
77124
// rewrite CSS variables
78-
if (cssVarRE.test(decl.value)) {
79-
decl.value = decl.value.replace(cssVarRE, (_, $1) => {
80-
return `var(--${genVarName(id, normalizeExpression($1), isProd)})`
81-
})
125+
const value = decl.value
126+
if (vBindRE.test(value)) {
127+
vBindRE.lastIndex = 0
128+
let transformed = ''
129+
let lastIndex = 0
130+
let match
131+
while ((match = vBindRE.exec(value))) {
132+
const start = match.index + match[0].length
133+
const end = lexBinding(value, start)
134+
if (end !== null) {
135+
const variable = normalizeExpression(value.slice(start, end))
136+
transformed +=
137+
value.slice(lastIndex, match.index) +
138+
`var(--${genVarName(id, variable, isProd)})`
139+
lastIndex = end + 1
140+
}
141+
}
142+
decl.value = transformed + value.slice(lastIndex)
82143
}
83144
}
84145
}

0 commit comments

Comments
 (0)