Skip to content

Commit 186e19d

Browse files
committed
fix: generate code should convert quote on vue template
1 parent e35908b commit 186e19d

7 files changed

Lines changed: 202 additions & 15 deletions

File tree

packages/vue-generator/src/generator/vue/sfc/generateAttribute.js

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const checkHasSpecialType = (obj) => {
7070
return false
7171
}
7272

73-
const handleJSExpressionBinding = (key, value, isJSX) => {
73+
const handleJSExpressionBinding = (key, value, isJSX, globalHooks) => {
7474
const expressValue = value.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '')
7575

7676
if (isJSX) {
@@ -85,7 +85,19 @@ const handleJSExpressionBinding = (key, value, isJSX) => {
8585
}
8686

8787
// expression 使用 v-bind 绑定
88-
return `:${key}="${expressValue}"`
88+
if (expressValue.includes('"')) {
89+
let stateKey = `${key}_${randomString()}`
90+
let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${expressValue}`)
91+
92+
while (!addSuccess) {
93+
stateKey = `${key}_${randomString()}`
94+
addSuccess = globalHooks.addState(stateKey, `${stateKey}:${expressValue}`)
95+
}
96+
97+
return `:${key}="state.${stateKey}"`
98+
} else {
99+
return `:${key}="${expressValue}"`
100+
}
89101
}
90102

91103
const handleBindI18n = (key, value, isJSX) => {
@@ -182,7 +194,19 @@ export const handleLoopAttrHook = (schemaData = {}, globalHooks, config) => {
182194
const iterVar = [...loopArgs]
183195

184196
if (!isJSX) {
185-
attributes.push(`v-for="(${iterVar.join(',')}) in ${source}"`)
197+
if (source.includes('"')) {
198+
let stateKey = `loop_${randomString()}`
199+
let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${source}`)
200+
201+
while (!addSuccess) {
202+
stateKey = `loop_${randomString()}`
203+
addSuccess = globalHooks.addState(stateKey, `${stateKey}:${source}`)
204+
}
205+
206+
attributes.push(`v-for="(${iterVar.join(',')}) in state.${stateKey}"`)
207+
} else {
208+
attributes.push(`v-for="(${iterVar.join(',')}) in ${source}"`)
209+
}
186210

187211
return
188212
}
@@ -352,7 +376,7 @@ export const handleExpressionAttrHook = (schemaData, globalHooks, config) => {
352376
Object.entries(props).forEach(([key, value]) => {
353377
if (value?.type === JS_EXPRESSION && !isOn(key)) {
354378
specialTypeHandler[JS_RESOURCE](value, globalHooks, config)
355-
attributes.push(handleJSExpressionBinding(key, value, isJSX))
379+
attributes.push(handleJSExpressionBinding(key, value, isJSX, globalHooks))
356380

357381
delete props[key]
358382
}
@@ -384,7 +408,7 @@ export const handleJSFunctionAttrHook = (schemaData, globalHooks, config) => {
384408
functionName = value.value
385409
}
386410

387-
attributes.push(handleJSExpressionBinding(key, { value: functionName }, isJSX))
411+
attributes.push(handleJSExpressionBinding(key, { value: functionName }, isJSX, globalHooks))
388412

389413
delete props[key]
390414
}
@@ -451,11 +475,15 @@ const genStateAccessor = (value, globalHooks) => {
451475
}
452476
}
453477

454-
const transformObjValue = (renderKey, value, globalHooks, config, transformObjType) => {
478+
const transformObjValue = (renderKey, value, globalHooks, config, transformObjType, shouldConvertQuote = false) => {
455479
const result = { shouldBindToState: false, res: null }
456480

457481
if (typeof value === 'string') {
458-
result.res = `${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"`
482+
if (shouldConvertQuote) {
483+
result.res = `${renderKey}'${value.replaceAll(/"/g, "'").replaceAll(/'/g, "\\'")}'`
484+
} else {
485+
result.res = `${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"`
486+
}
459487

460488
return result
461489
}
@@ -468,7 +496,11 @@ const transformObjValue = (renderKey, value, globalHooks, config, transformObjTy
468496

469497
if (specialTypeHandler[value?.type]) {
470498
const specialVal = specialTypeHandler[value.type](value, globalHooks, config)?.value || ''
471-
result.res = `${renderKey}${specialVal}`
499+
if (shouldConvertQuote) {
500+
result.res = `${renderKey}${specialVal.replaceAll(/"/g, "'")}`
501+
} else {
502+
result.res = `${renderKey}${specialVal}`
503+
}
472504

473505
if (specialTypes.includes(value.type)) {
474506
result.shouldBindToState = true
@@ -503,7 +535,7 @@ const transformObjValue = (renderKey, value, globalHooks, config, transformObjTy
503535
}
504536

505537
const normalKeyRegexp = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
506-
export const transformObjType = (obj, globalHooks, config) => {
538+
export const transformObjType = (obj, globalHooks, config, shouldConvertQuote = false) => {
507539
if (!obj || typeof obj !== 'object') {
508540
return {
509541
res: obj
@@ -527,7 +559,8 @@ export const transformObjType = (obj, globalHooks, config) => {
527559
value,
528560
globalHooks,
529561
config,
530-
transformObjType
562+
transformObjType,
563+
shouldConvertQuote
531564
)
532565

533566
if (tmpShouldBindToState) {
@@ -541,7 +574,7 @@ export const transformObjType = (obj, globalHooks, config) => {
541574

542575
// 复杂的 object 类型,需要递归处理
543576
const { res: tempRes, shouldBindToState: tempShouldBindToState } =
544-
transformObjType(value, globalHooks, config) || {}
577+
transformObjType(value, globalHooks, config, shouldConvertQuote) || {}
545578

546579
resStr.push(`${renderKey}${tempRes}`)
547580

@@ -570,7 +603,7 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => {
570603
return
571604
}
572605

573-
const { res, shouldBindToState } = transformObjType(value, globalHooks, config)
606+
const { res, shouldBindToState } = transformObjType(value, globalHooks, config, true)
574607

575608
if (shouldBindToState && !isJSX) {
576609
let stateKey = key
@@ -583,7 +616,7 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => {
583616

584617
attributes.push(`:${key}="state.${stateKey}"`)
585618
} else {
586-
attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res.replaceAll(/"/g, "'")}"`)
619+
attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res}"`)
587620
}
588621

589622
delete props[key]

packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const state = vue.reactive({
2626
nullValue: null,
2727
numberValue: 0,
2828
emptyStr: '',
29-
strVal: 'i am str.',
29+
strVal: "i am 'str'.",
3030
trueVal: true,
3131
falseVal: false,
3232
arrVal: [1, '2', { aaa: 'aaa' }, [3, 4], true, false],

packages/vue-generator/test/testcases/sfc/accessor/schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
}
4545
},
4646
"strVal": {
47-
"defaultValue": "i am str.",
47+
"defaultValue": "i am 'str'.",
4848
"accessor": {
4949
"getter": {
5050
"type": "JSFunction",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[
2+
{
3+
"componentName": "TinyButton",
4+
"exportName": "Button",
5+
"package": "@opentiny/vue",
6+
"version": "^3.10.0",
7+
"destructuring": true
8+
}
9+
]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<template>
2+
<div>
3+
<tiny-button
4+
v-for="(item, index) in state.loop_6cio"
5+
type="primary"
6+
text="test"
7+
subStr="pri'ma'ry'subStr'"
8+
:customExpressionTest="state.customExpressionTest_vBHN"
9+
:customAttrTest="{
10+
value: [
11+
{
12+
defaultValue: '{\'class\': \'test-class\', \'id\': \'test-id\', \'class2\': \'te\'st\'-class2\'}',
13+
subStr: 'test-\'cl\'ass2'
14+
}
15+
]
16+
}"
17+
></tiny-button>
18+
</div>
19+
</template>
20+
21+
<script setup>
22+
import * as vue from 'vue'
23+
import { defineProps, defineEmits } from 'vue'
24+
import { I18nInjectionKey } from 'vue-i18n'
25+
26+
const props = defineProps({})
27+
28+
const emit = defineEmits([])
29+
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
30+
const wrap = lowcodeWrap(props, { emit })
31+
wrap({ stores })
32+
33+
const state = vue.reactive({
34+
loop_6cio: [
35+
{
36+
type: 'primary',
37+
subStr: "primary'subStr'"
38+
},
39+
{
40+
type: ''
41+
},
42+
{
43+
type: 'info'
44+
},
45+
{
46+
type: 'success'
47+
},
48+
{
49+
type: 'warning'
50+
},
51+
{
52+
type: 'danger'
53+
}
54+
],
55+
customExpressionTest_vBHN: {
56+
value: [
57+
{
58+
defaultValue: '{"class": "test-class", "id": "test-id"}'
59+
}
60+
]
61+
},
62+
customAttrTest: { value: [{ defaultValue: "{'class': 'test-class', 'id': 'test-id'}" }] }
63+
})
64+
wrap({ state })
65+
</script>
66+
<style scoped></style>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"state": {
3+
"customAttrTest": {
4+
"value": [
5+
{
6+
"defaultValue": "{\"class\": \"test-class\", \"id\": \"test-id\"}"
7+
}
8+
]
9+
}
10+
},
11+
"methods": {},
12+
"componentName": "Page",
13+
"css": "",
14+
"props": {},
15+
"lifeCycles": {},
16+
"children": [
17+
{
18+
"componentName": "TinyButton",
19+
"props": {
20+
"type": "primary",
21+
"text": "test",
22+
"subStr": "pri\"ma\"ry'subStr'",
23+
"customAttrTest": {
24+
"value": [
25+
{
26+
"defaultValue": "{\"class\": \"test-class\", \"id\": \"test-id\", \"class2\": \"te'st'-class2\"}",
27+
"subStr": "test-'cl'ass2"
28+
}
29+
]
30+
},
31+
"customExpressionTest": {
32+
"type": "JSExpression",
33+
"value": "{\n \"value\": [\n {\n \"defaultValue\": \"{\\\"class\\\": \\\"test-class\\\", \\\"id\\\": \\\"test-id\\\"}\"\n }\n ]\n}"
34+
}
35+
},
36+
"loopArgs": ["item", "index"],
37+
"loop": {
38+
"type": "JSExpression",
39+
"value": "[\n {\n type: 'primary'\n, subStr: \"primary'subStr'\"\n },\n {\n type: \"\"\n },\n {\n type: \"info\"\n },\n {\n type: \"success\"\n },\n {\n type: \"warning\"\n },\n {\n type: \"danger\"\n }\n]"
40+
},
41+
"id": "63623253",
42+
"children": []
43+
}
44+
],
45+
"fileName": "testTemplateQuote"
46+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { expect, test, beforeEach, afterEach, vi } from 'vitest'
2+
import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc'
3+
import pageSchema from './page.schema.json'
4+
import { formatCode } from '@/utils/formatCode'
5+
6+
let count = 0
7+
const mockValue = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
8+
9+
beforeEach(() => {
10+
// 伪随机数,保证每次快照都一致
11+
vi.spyOn(global.Math, 'random').mockImplementation(() => {
12+
const res = mockValue[count]
13+
14+
count++
15+
if (count > 10) {
16+
count = 0
17+
}
18+
19+
return res
20+
})
21+
})
22+
23+
afterEach(() => {
24+
vi.spyOn(global.Math, 'random').mockRestore()
25+
})
26+
27+
test('should generate template quote correctly', async () => {
28+
const res = genSFCWithDefaultPlugin(pageSchema, [])
29+
30+
const formattedCode = formatCode(res, 'vue')
31+
32+
await expect(formattedCode).toMatchFileSnapshot('./expected/templateQuote.vue')
33+
})

0 commit comments

Comments
 (0)