Skip to content

Commit 8b4b104

Browse files
committed
feat(vue-generator): optimize quote escaping in template attributes and unique state key generation
1. 引号转义优化:将 handleExpressionAttrHook 和 handlePrimitiveAttributeHook 中的引号处理逻辑由替换为单引号改为替换为 HTML 实体 "。这确保了在生成的 Vue 模板中,属性值内的双引号能够被正确转义,避免模板解析错误。 2. 增强鲁棒性:在 generateAttribute.js 的循环中增加了 retryCount 限制(100次),防止在生成唯一的 stateKey 时出现潜在的死循环。
1 parent 3b78770 commit 8b4b104

13 files changed

Lines changed: 329 additions & 93 deletions

File tree

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

Lines changed: 23 additions & 48 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, globalHooks) => {
73+
const handleJSExpressionBinding = (key, value, isJSX) => {
7474
const expressValue = value.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '')
7575

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

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

10394
const handleBindI18n = (key, value, isJSX) => {
@@ -194,19 +185,7 @@ export const handleLoopAttrHook = (schemaData = {}, globalHooks, config) => {
194185
const iterVar = [...loopArgs]
195186

196187
if (!isJSX) {
197-
if (source.includes('"') && 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.replaceAll(/"/g, "'")}"`)
209-
}
188+
attributes.push(`v-for="(${iterVar.join(',')}) in ${source.replaceAll(/"/g, '"')}"`)
210189

211190
return
212191
}
@@ -376,7 +355,7 @@ export const handleExpressionAttrHook = (schemaData, globalHooks, config) => {
376355
Object.entries(props).forEach(([key, value]) => {
377356
if (value?.type === JS_EXPRESSION && !isOn(key)) {
378357
specialTypeHandler[JS_RESOURCE](value, globalHooks, config)
379-
attributes.push(handleJSExpressionBinding(key, value, isJSX, globalHooks))
358+
attributes.push(handleJSExpressionBinding(key, value, isJSX))
380359

381360
delete props[key]
382361
}
@@ -408,7 +387,7 @@ export const handleJSFunctionAttrHook = (schemaData, globalHooks, config) => {
408387
functionName = value.value
409388
}
410389

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

413392
delete props[key]
414393
}
@@ -475,15 +454,15 @@ const genStateAccessor = (value, globalHooks) => {
475454
}
476455
}
477456

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

481460
if (typeof value === 'string') {
482-
if (shouldConvertQuote) {
483-
result.res = `${renderKey}'${value.replaceAll(/"/g, "'").replaceAll(/'/g, "\\'")}'`
484-
} else {
485-
result.res = `${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"`
486-
}
461+
result.res = `${renderKey}"${value
462+
.replaceAll(/\\/g, '\\\\')
463+
.replaceAll(/\n/g, '\\n')
464+
.replaceAll(/\r/g, '\\r')
465+
.replaceAll(/"/g, '\\"')}"`
487466

488467
return result
489468
}
@@ -496,11 +475,7 @@ const transformObjValue = (renderKey, value, globalHooks, config, transformObjTy
496475

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

505480
if (specialTypes.includes(value.type)) {
506481
result.shouldBindToState = true
@@ -535,7 +510,7 @@ const transformObjValue = (renderKey, value, globalHooks, config, transformObjTy
535510
}
536511

537512
const normalKeyRegexp = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
538-
export const transformObjType = (obj, globalHooks, config, shouldConvertQuote = false) => {
513+
export const transformObjType = (obj, globalHooks, config) => {
539514
if (!obj || typeof obj !== 'object') {
540515
return {
541516
res: obj
@@ -559,8 +534,7 @@ export const transformObjType = (obj, globalHooks, config, shouldConvertQuote =
559534
value,
560535
globalHooks,
561536
config,
562-
transformObjType,
563-
shouldConvertQuote
537+
transformObjType
564538
)
565539

566540
if (tmpShouldBindToState) {
@@ -574,7 +548,7 @@ export const transformObjType = (obj, globalHooks, config, shouldConvertQuote =
574548

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

579553
resStr.push(`${renderKey}${tempRes}`)
580554

@@ -603,20 +577,21 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => {
603577
return
604578
}
605579

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

608582
if (shouldBindToState && !isJSX) {
609583
let stateKey = key
610584
let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${res}`)
585+
let retryCount = 0
611586

612-
while (!addSuccess) {
587+
while (!addSuccess && retryCount++ < 100) {
613588
stateKey = `${key}${randomString()}`
614589
addSuccess = globalHooks.addState(stateKey, `${stateKey}:${res}`)
615590
}
616591

617592
attributes.push(`:${key}="state.${stateKey}"`)
618593
} else {
619-
attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res}"`)
594+
attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res.replaceAll(/"/g, '&quot;')}"`)
620595
}
621596

622597
delete props[key]
@@ -633,7 +608,7 @@ export const handlePrimitiveAttributeHook = (schemaData, globalHooks, config) =>
633608
const valueType = typeof value
634609

635610
if (valueType === 'string') {
636-
attributes.push(`${key}="${value.replaceAll(/"/g, "'")}"`)
611+
attributes.push(`${key}="${value.replaceAll(/"/g, '&quot;')}"`)
637612

638613
delete props[key]
639614
}

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: 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>

packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,42 @@
11
<template>
22
<div>
33
<tiny-button
4-
v-for="(item, index) in state.loop_6cio"
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+
]"
525
type="primary"
626
text="test"
7-
subStr="pri'ma'ry'subStr'"
27+
subStr="pri&quot;ma&quot;ry'subStr'"
828
:customExpressionTest="{
929
value: [
1030
{
11-
defaultValue: '{\'class\': \'test-class\', \'id\': \'test-id\'}'
31+
defaultValue: '{&quot;class&quot;: &quot;test-class&quot;, &quot;id&quot;: &quot;test-id&quot;}'
1232
}
1333
]
1434
}"
1535
:customAttrTest="{
1636
value: [
1737
{
18-
defaultValue: '{\'class\': \'test-class\', \'id\': \'test-id\', \'class2\': \'te\'st\'-class2\'}',
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;}',
1940
subStr: 'test-\'cl\'ass2'
2041
}
2142
]
@@ -37,28 +58,7 @@ const wrap = lowcodeWrap(props, { emit })
3758
wrap({ stores })
3859
3960
const state = vue.reactive({
40-
loop_6cio: [
41-
{
42-
type: 'primary',
43-
subStr: "primary'subStr'"
44-
},
45-
{
46-
type: ''
47-
},
48-
{
49-
type: 'info'
50-
},
51-
{
52-
type: 'success'
53-
},
54-
{
55-
type: 'warning'
56-
},
57-
{
58-
type: 'danger'
59-
}
60-
],
61-
customAttrTest: { value: [{ defaultValue: "{'class': 'test-class', 'id': 'test-id'}" }] }
61+
customAttrTest: { value: [{ defaultValue: '{"class": "test-class", "id": "test-id"}' }] }
6262
})
6363
wrap({ state })
6464
</script>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[
2+
{
3+
"componentName": "TinyGrid",
4+
"exportName": "Grid",
5+
"package": "@opentiny/vue",
6+
"version": "^3.10.0",
7+
"destructuring": true
8+
}
9+
]

0 commit comments

Comments
 (0)