Skip to content

Commit e0821bc

Browse files
authored
fix: generate code should convert quote on vue template (#1464)
1 parent 9f888ac commit e0821bc

File tree

17 files changed

+438
-11
lines changed

17 files changed

+438
-11
lines changed

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

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ const handleJSExpressionBinding = (key, value, isJSX) => {
8585
}
8686

8787
// expression 使用 v-bind 绑定
88-
return `:${key}="${expressValue}"`
88+
// 如果包含双引号,通过 " 编码避免与属性分隔符冲突
89+
// 比如绑定的值为:[{ "name": "test" }]
90+
// 则转换为: :key="[{ "name": "test" }]"
91+
return `:${key}="${expressValue.replaceAll(/"/g, '"')}"`
8992
}
9093

9194
const handleBindI18n = (key, value, isJSX) => {
@@ -176,13 +179,13 @@ export const handleLoopAttrHook = (schemaData = {}, globalHooks, config) => {
176179
if (loop?.value && loop?.type) {
177180
source = loop.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '')
178181
} else {
179-
source = JSON.stringify(loop).replaceAll("'", "\\'").replaceAll(/"/g, "'")
182+
source = JSON.stringify(loop)
180183
}
181184

182185
const iterVar = [...loopArgs]
183186

184187
if (!isJSX) {
185-
attributes.push(`v-for="(${iterVar.join(',')}) in ${source}"`)
188+
attributes.push(`v-for="(${iterVar.join(',')}) in ${source.replaceAll(/"/g, '"')}"`)
186189

187190
return
188191
}
@@ -455,7 +458,11 @@ const transformObjValue = (renderKey, value, globalHooks, config, transformObjTy
455458
const result = { shouldBindToState: false, res: null }
456459

457460
if (typeof value === 'string') {
458-
result.res = `${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"`
461+
result.res = `${renderKey}"${value
462+
.replaceAll(/\\/g, '\\\\')
463+
.replaceAll(/\n/g, '\\n')
464+
.replaceAll(/\r/g, '\\r')
465+
.replaceAll(/"/g, '\\"')}"`
459466

460467
return result
461468
}
@@ -575,15 +582,21 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => {
575582
if (shouldBindToState && !isJSX) {
576583
let stateKey = key
577584
let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${res}`)
585+
let retryCount = 0
578586

579-
while (!addSuccess) {
587+
while (!addSuccess && retryCount++ < 100) {
580588
stateKey = `${key}${randomString()}`
581589
addSuccess = globalHooks.addState(stateKey, `${stateKey}:${res}`)
582590
}
583591

584-
attributes.push(`:${key}="state.${stateKey}"`)
592+
if (addSuccess) {
593+
attributes.push(`:${key}="state.${stateKey}"`)
594+
} else {
595+
// state 注册失败,回退到内联绑定
596+
attributes.push(`:${key}="${res.replaceAll(/"/g, '&quot;')}"`)
597+
}
585598
} else {
586-
attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res.replaceAll(/"/g, "'")}"`)
599+
attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res.replaceAll(/"/g, '&quot;')}"`)
587600
}
588601

589602
delete props[key]
@@ -600,7 +613,7 @@ export const handlePrimitiveAttributeHook = (schemaData, globalHooks, config) =>
600613
const valueType = typeof value
601614

602615
if (valueType === 'string') {
603-
attributes.push(`${key}="${value.replaceAll(/"/g, "'")}"`)
616+
attributes.push(`${key}="${value.replaceAll(/"/g, '&quot;')}"`)
604617

605618
delete props[key]
606619
}

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",

packages/vue-generator/test/testcases/sfc/case01/expected/FormTable.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ const { utils } = wrap(function () {
118118
})()
119119
const state = vue.reactive({
120120
IconPlusSquare: utils.IconPlusSquare(),
121-
theme: "{ 'id': 22, 'name': '@cloud/tinybuilder-theme-dark', 'description': '黑暗主题' }",
121+
theme: '{ "id": 22, "name": "@cloud/tinybuilder-theme-dark", "description": "黑暗主题" }',
122122
companyName: '',
123123
companyOptions: null,
124124
companyCity: '',
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<template>
2+
<div>
3+
<tiny-grid :columns="state.columns"></tiny-grid>
4+
</div>
5+
</template>
6+
7+
<script setup lang="jsx">
8+
import { Grid as TinyGrid } from '@opentiny/vue'
9+
import * as vue from 'vue'
10+
import { defineProps, defineEmits } from 'vue'
11+
import { I18nInjectionKey } from 'vue-i18n'
12+
13+
const props = defineProps({})
14+
15+
const emit = defineEmits([])
16+
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
17+
const wrap = lowcodeWrap(props, { emit })
18+
wrap({ stores })
19+
20+
const state = vue.reactive({
21+
columns: [
22+
{ field: 'info', title: '信息', slots: { default: ({ row }, h) => <span title='{"key": "value"}'></span> } }
23+
]
24+
})
25+
wrap({ state })
26+
</script>
27+
<style scoped></style>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<template>
2+
<div>
3+
<tiny-button
4+
multilineJson='{
5+
"name": "test",
6+
"value": "data"
7+
}'
8+
:objWithMultiline="{ template: 'line1\nline2', label: 'normal' }"
9+
></tiny-button>
10+
</div>
11+
</template>
12+
13+
<script setup>
14+
import * as vue from 'vue'
15+
import { defineProps, defineEmits } from 'vue'
16+
import { I18nInjectionKey } from 'vue-i18n'
17+
18+
const props = defineProps({})
19+
20+
const emit = defineEmits([])
21+
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
22+
const wrap = lowcodeWrap(props, { emit })
23+
wrap({ stores })
24+
25+
const state = vue.reactive({})
26+
wrap({ state })
27+
</script>
28+
<style scoped></style>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<template>
2+
<div>
3+
<tiny-button
4+
noQuotes="plain text value"
5+
onlyDouble='{"name": "test", "id": "main"}'
6+
onlySingle="it's a 'test' value"
7+
bothQuotes='She said "hello" and it&apos;s fine'
8+
htmlAttr='class="primary" id="btn-1"'
9+
emptyStr=""
10+
></tiny-button>
11+
</div>
12+
</template>
13+
14+
<script setup>
15+
import * as vue from 'vue'
16+
import { defineProps, defineEmits } from 'vue'
17+
import { I18nInjectionKey } from 'vue-i18n'
18+
19+
const props = defineProps({})
20+
21+
const emit = defineEmits([])
22+
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
23+
const wrap = lowcodeWrap(props, { emit })
24+
wrap({ stores })
25+
26+
const state = vue.reactive({})
27+
wrap({ state })
28+
</script>
29+
<style scoped></style>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<template>
2+
<div>
3+
<tiny-button
4+
jsonContent='{"key": "value", "nested": "data"}'
5+
mixedQuotes="She said &quot;hello&quot; and he said 'hi'"
6+
:filteredItems="state.items.filter((i) => i.name === 'test' && i.tag === 'featured')"
7+
></tiny-button>
8+
</div>
9+
</template>
10+
11+
<script setup>
12+
import * as vue from 'vue'
13+
import { defineProps, defineEmits } from 'vue'
14+
import { I18nInjectionKey } from 'vue-i18n'
15+
16+
const props = defineProps({})
17+
18+
const emit = defineEmits([])
19+
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
20+
const wrap = lowcodeWrap(props, { emit })
21+
wrap({ stores })
22+
23+
const state = vue.reactive({
24+
items: [
25+
{ name: 'test', tag: 'featured' },
26+
{ name: 'hello', tag: 'normal' }
27+
]
28+
})
29+
wrap({ state })
30+
</script>
31+
<style scoped></style>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<template>
2+
<div>
3+
<tiny-button
4+
v-for="(item, index) in [
5+
{
6+
type: 'primary',
7+
subStr: 'primary\'subStr\''
8+
},
9+
{
10+
type: ''
11+
},
12+
{
13+
type: 'info'
14+
},
15+
{
16+
type: 'success'
17+
},
18+
{
19+
type: 'warning'
20+
},
21+
{
22+
type: 'danger'
23+
}
24+
]"
25+
type="primary"
26+
text="test"
27+
subStr="pri&quot;ma&quot;ry'subStr'"
28+
:customExpressionTest="{
29+
value: [
30+
{
31+
defaultValue: '{&quot;class&quot;: &quot;test-class&quot;, &quot;id&quot;: &quot;test-id&quot;}'
32+
}
33+
]
34+
}"
35+
:customAttrTest="{
36+
value: [
37+
{
38+
defaultValue:
39+
'{&quot;class&quot;: &quot;test-class&quot;, &quot;id&quot;: &quot;test-id&quot;, &quot;class2&quot;: &quot;te\'st\'-class2&quot;}',
40+
subStr: 'test-\'cl\'ass2'
41+
}
42+
]
43+
}"
44+
></tiny-button>
45+
</div>
46+
</template>
47+
48+
<script setup>
49+
import * as vue from 'vue'
50+
import { defineProps, defineEmits } from 'vue'
51+
import { I18nInjectionKey } from 'vue-i18n'
52+
53+
const props = defineProps({})
54+
55+
const emit = defineEmits([])
56+
const { t, lowcodeWrap, stores } = vue.inject(I18nInjectionKey).lowcode()
57+
const wrap = lowcodeWrap(props, { emit })
58+
wrap({ stores })
59+
60+
const state = vue.reactive({
61+
customAttrTest: { value: [{ defaultValue: '{"class": "test-class", "id": "test-id"}' }] }
62+
})
63+
wrap({ state })
64+
</script>
65+
<style scoped></style>

0 commit comments

Comments
 (0)