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'
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`])
})
})
})

View 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)
}
}
}