fix(compiler-sfc): improve css v-bind parsing

fix #6022
This commit is contained in:
Evan You 2022-06-06 20:02:08 +08:00
parent 9734b31c31
commit e60244bcdf
2 changed files with 88 additions and 11 deletions

View File

@ -1,4 +1,4 @@
import { compileStyle } from '../src' import { compileStyle, parse } from '../src'
import { mockId, compileSFCScript, assertCode } from './utils' import { mockId, compileSFCScript, assertCode } from './utils'
describe('CSS vars injection', () => { describe('CSS vars injection', () => {
@ -231,5 +231,21 @@ describe('CSS vars injection', () => {
})`) })`)
assertCode(content) 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`])
})
}) })
}) })

View File

@ -12,8 +12,6 @@ import { PluginCreator } from 'postcss'
import hash from 'hash-sum' import hash from 'hash-sum'
export const CSS_VARS_HELPER = `useCssVars` 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( export function genCssVarsFromList(
vars: string[], vars: string[],
@ -47,22 +45,71 @@ function normalizeExpression(exp: string) {
return exp return exp
} }
const vBindRE = /v-bind\s*\(/g
export function parseCssVars(sfc: SFCDescriptor): string[] { export function parseCssVars(sfc: SFCDescriptor): string[] {
const vars: string[] = [] const vars: string[] = []
sfc.styles.forEach(style => { sfc.styles.forEach(style => {
let match let match
// ignore v-bind() in comments /* ... */ // ignore v-bind() in comments /* ... */
const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '') const content = style.content.replace(/\/\*([\s\S]*?)\*\//g, '')
while ((match = cssVarRE.exec(content))) { while ((match = vBindRE.exec(content))) {
const variable = normalizeExpression(match[1]) 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)) { if (!vars.includes(variable)) {
vars.push(variable) vars.push(variable)
} }
} }
}
}) })
return vars 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 // for compileStyle
export interface CssVarsPluginOptions { export interface CssVarsPluginOptions {
id: string id: string
@ -75,10 +122,24 @@ export const cssVarsPlugin: PluginCreator<CssVarsPluginOptions> = opts => {
postcssPlugin: 'vue-sfc-vars', postcssPlugin: 'vue-sfc-vars',
Declaration(decl) { Declaration(decl) {
// rewrite CSS variables // rewrite CSS variables
if (cssVarRE.test(decl.value)) { const value = decl.value
decl.value = decl.value.replace(cssVarRE, (_, $1) => { if (vBindRE.test(value)) {
return `var(--${genVarName(id, normalizeExpression($1), isProd)})` 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)
} }
} }
} }