diff --git a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js index 2f4c8e898e..2e147e3d33 100644 --- a/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js +++ b/packages/vue-generator/src/generator/vue/sfc/generateAttribute.js @@ -70,7 +70,7 @@ export const checkHasSpecialType = (obj) => { return false } -const handleJSExpressionBinding = (key, value, isJSX) => { +const handleJSExpressionBinding = (key, value, isJSX, globalHooks) => { const expressValue = value.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '') if (isJSX) { @@ -85,7 +85,19 @@ const handleJSExpressionBinding = (key, value, isJSX) => { } // expression 使用 v-bind 绑定 - return `:${key}="${expressValue}"` + if (expressValue.includes('"') && expressValue.includes("'")) { + let stateKey = `${key}_${randomString()}` + let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${expressValue}`) + + while (!addSuccess) { + stateKey = `${key}_${randomString()}` + addSuccess = globalHooks.addState(stateKey, `${stateKey}:${expressValue}`) + } + + return `:${key}="state.${stateKey}"` + } else { + return `:${key}="${expressValue.replaceAll(/"/g, "'")}"` + } } const handleBindI18n = (key, value, isJSX) => { @@ -176,13 +188,25 @@ export const handleLoopAttrHook = (schemaData = {}, globalHooks, config) => { if (loop?.value && loop?.type) { source = loop.value.replace(isJSX ? thisRegexp : thisPropsBindRe, '') } else { - source = JSON.stringify(loop).replaceAll("'", "\\'").replaceAll(/"/g, "'") + source = JSON.stringify(loop) } const iterVar = [...loopArgs] if (!isJSX) { - attributes.push(`v-for="(${iterVar.join(',')}) in ${source}"`) + if (source.includes('"') && source.includes("'")) { + let stateKey = `loop_${randomString()}` + let addSuccess = globalHooks.addState(stateKey, `${stateKey}:${source}`) + + while (!addSuccess) { + stateKey = `loop_${randomString()}` + addSuccess = globalHooks.addState(stateKey, `${stateKey}:${source}`) + } + + attributes.push(`v-for="(${iterVar.join(',')}) in state.${stateKey}"`) + } else { + attributes.push(`v-for="(${iterVar.join(',')}) in ${source.replaceAll(/"/g, "'")}"`) + } return } @@ -352,7 +376,7 @@ export const handleExpressionAttrHook = (schemaData, globalHooks, config) => { Object.entries(props).forEach(([key, value]) => { if (value?.type === JS_EXPRESSION && !isOn(key)) { specialTypeHandler[JS_RESOURCE](value, globalHooks, config) - attributes.push(handleJSExpressionBinding(key, value, isJSX)) + attributes.push(handleJSExpressionBinding(key, value, isJSX, globalHooks)) delete props[key] } @@ -384,7 +408,7 @@ export const handleJSFunctionAttrHook = (schemaData, globalHooks, config) => { functionName = value.value } - attributes.push(handleJSExpressionBinding(key, { value: functionName }, isJSX)) + attributes.push(handleJSExpressionBinding(key, { value: functionName }, isJSX, globalHooks)) delete props[key] } @@ -451,11 +475,15 @@ const genStateAccessor = (value, globalHooks) => { } } -const transformObjValue = (renderKey, value, globalHooks, config, transformObjType) => { +const transformObjValue = (renderKey, value, globalHooks, config, transformObjType, shouldConvertQuote = false) => { const result = { shouldBindToState: false, res: null } if (typeof value === 'string') { - result.res = `${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"` + if (shouldConvertQuote) { + result.res = `${renderKey}'${value.replaceAll(/"/g, "'").replaceAll(/'/g, "\\'")}'` + } else { + result.res = `${renderKey}"${value.replaceAll("'", "\\'").replaceAll(/"/g, "'")}"` + } return result } @@ -468,7 +496,11 @@ const transformObjValue = (renderKey, value, globalHooks, config, transformObjTy if (specialTypeHandler[value?.type]) { const specialVal = specialTypeHandler[value.type](value, globalHooks, config)?.value || '' - result.res = `${renderKey}${specialVal}` + if (shouldConvertQuote) { + result.res = `${renderKey}${specialVal.replaceAll(/"/g, "'")}` + } else { + result.res = `${renderKey}${specialVal}` + } if (specialTypes.includes(value.type)) { result.shouldBindToState = true @@ -503,7 +535,7 @@ const transformObjValue = (renderKey, value, globalHooks, config, transformObjTy } const normalKeyRegexp = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/ -export const transformObjType = (obj, globalHooks, config) => { +export const transformObjType = (obj, globalHooks, config, shouldConvertQuote = false) => { if (!obj || typeof obj !== 'object') { return { res: obj @@ -527,7 +559,8 @@ export const transformObjType = (obj, globalHooks, config) => { value, globalHooks, config, - transformObjType + transformObjType, + shouldConvertQuote ) if (tmpShouldBindToState) { @@ -541,7 +574,7 @@ export const transformObjType = (obj, globalHooks, config) => { // 复杂的 object 类型,需要递归处理 const { res: tempRes, shouldBindToState: tempShouldBindToState } = - transformObjType(value, globalHooks, config) || {} + transformObjType(value, globalHooks, config, shouldConvertQuote) || {} resStr.push(`${renderKey}${tempRes}`) @@ -570,7 +603,7 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => { return } - const { res, shouldBindToState } = transformObjType(value, globalHooks, config) + const { res, shouldBindToState } = transformObjType(value, globalHooks, config, true) if (shouldBindToState && !isJSX) { let stateKey = key @@ -583,7 +616,7 @@ export const handleObjBindAttrHook = (schemaData, globalHooks, config) => { attributes.push(`:${key}="state.${stateKey}"`) } else { - attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res.replaceAll(/"/g, "'")}"`) + attributes.push(isJSX ? `${key}={${res}}` : `:${key}="${res}"`) } delete props[key] diff --git a/packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue b/packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue index c515a9711d..b524540906 100644 --- a/packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue +++ b/packages/vue-generator/test/testcases/sfc/accessor/expected/Accessor.vue @@ -26,7 +26,7 @@ const state = vue.reactive({ nullValue: null, numberValue: 0, emptyStr: '', - strVal: 'i am str.', + strVal: "i am 'str'.", trueVal: true, falseVal: false, arrVal: [1, '2', { aaa: 'aaa' }, [3, 4], true, false], diff --git a/packages/vue-generator/test/testcases/sfc/accessor/schema.json b/packages/vue-generator/test/testcases/sfc/accessor/schema.json index 204121eec1..0935a984a8 100644 --- a/packages/vue-generator/test/testcases/sfc/accessor/schema.json +++ b/packages/vue-generator/test/testcases/sfc/accessor/schema.json @@ -44,7 +44,7 @@ } }, "strVal": { - "defaultValue": "i am str.", + "defaultValue": "i am 'str'.", "accessor": { "getter": { "type": "JSFunction", diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/components-map.json b/packages/vue-generator/test/testcases/sfc/templateQuote/components-map.json new file mode 100644 index 0000000000..b84fe3b474 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/templateQuote/components-map.json @@ -0,0 +1,9 @@ +[ + { + "componentName": "TinyButton", + "exportName": "Button", + "package": "@opentiny/vue", + "version": "^3.10.0", + "destructuring": true + } +] diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue b/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue new file mode 100644 index 0000000000..27197c4583 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/templateQuote/expected/templateQuote.vue @@ -0,0 +1,65 @@ + + + + diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/page.schema.json b/packages/vue-generator/test/testcases/sfc/templateQuote/page.schema.json new file mode 100644 index 0000000000..534af857a8 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/templateQuote/page.schema.json @@ -0,0 +1,46 @@ +{ + "state": { + "customAttrTest": { + "value": [ + { + "defaultValue": "{\"class\": \"test-class\", \"id\": \"test-id\"}" + } + ] + } + }, + "methods": {}, + "componentName": "Page", + "css": "", + "props": {}, + "lifeCycles": {}, + "children": [ + { + "componentName": "TinyButton", + "props": { + "type": "primary", + "text": "test", + "subStr": "pri\"ma\"ry'subStr'", + "customAttrTest": { + "value": [ + { + "defaultValue": "{\"class\": \"test-class\", \"id\": \"test-id\", \"class2\": \"te'st'-class2\"}", + "subStr": "test-'cl'ass2" + } + ] + }, + "customExpressionTest": { + "type": "JSExpression", + "value": "{\n \"value\": [\n {\n \"defaultValue\": \"{\\\"class\\\": \\\"test-class\\\", \\\"id\\\": \\\"test-id\\\"}\"\n }\n ]\n}" + } + }, + "loopArgs": ["item", "index"], + "loop": { + "type": "JSExpression", + "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]" + }, + "id": "63623253", + "children": [] + } + ], + "fileName": "testTemplateQuote" +} diff --git a/packages/vue-generator/test/testcases/sfc/templateQuote/templateQuote.test.js b/packages/vue-generator/test/testcases/sfc/templateQuote/templateQuote.test.js new file mode 100644 index 0000000000..4f6d0672e4 --- /dev/null +++ b/packages/vue-generator/test/testcases/sfc/templateQuote/templateQuote.test.js @@ -0,0 +1,33 @@ +import { expect, test, beforeEach, afterEach, vi } from 'vitest' +import { genSFCWithDefaultPlugin } from '@/generator/vue/sfc' +import pageSchema from './page.schema.json' +import { formatCode } from '@/utils/formatCode' + +let count = 0 +const mockValue = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] + +beforeEach(() => { + // 伪随机数,保证每次快照都一致 + vi.spyOn(global.Math, 'random').mockImplementation(() => { + const res = mockValue[count] + + count++ + if (count > 10) { + count = 0 + } + + return res + }) +}) + +afterEach(() => { + vi.spyOn(global.Math, 'random').mockRestore() +}) + +test('should generate template quote correctly', async () => { + const res = genSFCWithDefaultPlugin(pageSchema, []) + + const formattedCode = formatCode(res, 'vue') + + await expect(formattedCode).toMatchFileSnapshot('./expected/templateQuote.vue') +})