Skip to content

Commit 91fcbcf

Browse files
committed
Add string literal replacement for class bindings in Vite plugin
1 parent b3c09b3 commit 91fcbcf

2 files changed

Lines changed: 104 additions & 11 deletions

File tree

src/vite/index.js

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,66 @@
11
import { invalidChars } from '../lib/adaptMiniPrograms'
22
import tailwindcss from '../index'
33

4+
/**
5+
* 从 JavaScript 表达式中提取所有字符串字面量并替换
6+
* 支持数组、对象、三元运算符等复杂语法
7+
*/
8+
function replaceStringLiterals(expression, escapeFn) {
9+
const replacements = []
10+
let i = 0
11+
let inString = false
12+
let stringChar = null
13+
let stringStart = 0
14+
let escaped = false
15+
16+
// 第一步:找到所有字符串字面量的位置
17+
while (i < expression.length) {
18+
const char = expression[i]
19+
20+
if (!inString) {
21+
// 检查是否是字符串开始(单引号或双引号)
22+
if ((char === '"' || char === "'") && !escaped) {
23+
inString = true
24+
stringChar = char
25+
stringStart = i
26+
escaped = false
27+
} else {
28+
escaped = char === '\\' && !escaped
29+
}
30+
} else {
31+
// 在字符串内部
32+
if (char === '\\' && !escaped) {
33+
escaped = true
34+
} else if (char === stringChar && !escaped) {
35+
// 字符串结束,记录需要替换的内容
36+
const stringContent = expression.slice(stringStart + 1, i)
37+
const escapedContent = escapeFn(stringContent)
38+
replacements.push({
39+
start: stringStart + 1,
40+
end: i,
41+
replacement: escapedContent,
42+
})
43+
inString = false
44+
stringChar = null
45+
escaped = false
46+
} else {
47+
escaped = false
48+
}
49+
}
50+
51+
i++
52+
}
53+
54+
// 第二步:从后向前替换,避免位置偏移
55+
let result = expression
56+
for (let j = replacements.length - 1; j >= 0; j--) {
57+
const { start, end, replacement } = replacements[j]
58+
result = result.slice(0, start) + replacement + result.slice(end)
59+
}
60+
61+
return result
62+
}
63+
464
export default function modifyClasses() {
565
return {
666
name: 'modify-classes',
@@ -20,20 +80,43 @@ export default function modifyClasses() {
2080
return
2181
}
2282

23-
const re = new RegExp(
24-
`[${Array.from(invalidChars)
25-
.map((char) => `\\${char}`)
26-
.join('')}]`,
27-
'g'
28-
)
29-
// 替换所有 class="..."
30-
const transformed = code.replace(/class\s*=\s*"([^"]+)"/g, (_, classValue) => {
31-
const newClasses = classValue
83+
// 构建转义正则,转义字符类中需要转义的特殊字符
84+
// 在字符类中,需要转义的字符有: ] \ - ^
85+
const escapedChars = Array.from(invalidChars)
86+
.map((char) => {
87+
// 在字符类中,] 和 \ 需要转义
88+
if (char === ']' || char === '\\') {
89+
return `\\${char}`
90+
}
91+
return char
92+
})
93+
.join('')
94+
const re = new RegExp(`[${escapedChars}]`, 'g')
95+
96+
// 转义类名中的特殊字符
97+
const escapeClassName = (className) => className.replace(re, '_')
98+
99+
// 转义类名字符串中的类名
100+
const escapeClassString = (classString) => {
101+
return classString
32102
.split(/\s+/)
33-
.map((cls) => cls.replace(re, '_'))
103+
.map((cls) => escapeClassName(cls))
34104
.join(' ')
35-
return `class="${newClasses}"`
105+
}
106+
107+
let transformed = code
108+
109+
// 替换 class="..." (但不匹配 :class)
110+
transformed = transformed.replace(/(^|[^:])(\s*)class\s*=\s*"([^"]+)"/g, (match, before, space, classValue) => {
111+
return `${before}${space}class="${escapeClassString(classValue)}"`
112+
})
113+
114+
// 使用 AST 解析 :class 绑定中的 JavaScript 表达式
115+
transformed = transformed.replace(/:class\s*=\s*"([^"]+)"/g, (match, expression) => {
116+
const escapedExpression = replaceStringLiterals(expression, escapeClassString)
117+
return `:class="${escapedExpression}"`
36118
})
119+
37120
return transformed
38121
},
39122
}

tests/mini-programs.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ test('vite plugin', async () => {
126126
<div class="first:mt-[20rpx] hover:bg-gray-100"></div>
127127
<div class="h-[80.5%]"></div>
128128
<div class="w-1/2"></div>
129+
130+
<!-- vue language -->
131+
<div :class="['space-x-3 w-1/2', 'h-[80.5%]']"></div>
132+
<div :class="isActive ? 'w-1/2' : 'w-1/3'"></div>
133+
<div :class="{ 'w-1/2': isActive, 'w-1/3': !isActive }"></div>
129134
</template>
130135
`
131136
expect(modifyClasses().transform(source, 'App.vue')).toMatchInlineSnapshot(`
@@ -134,6 +139,11 @@ test('vite plugin', async () => {
134139
<div class="first_mt-_20rpx_ hover_bg-gray-100"></div>
135140
<div class="h-_80_5__"></div>
136141
<div class="w-1_2"></div>
142+
143+
<!-- vue language -->
144+
<div :class="['space-x-3 w-1_2', 'h-_80_5__']"></div>
145+
<div :class="isActive ? 'w-1_2' : 'w-1_3'"></div>
146+
<div :class="{ 'w-1_2': isActive, 'w-1_3': !isActive }"></div>
137147
</template>
138148
"
139149
`)

0 commit comments

Comments
 (0)