From ff8fcd2e2b95694527018f7836bab781f8600d25 Mon Sep 17 00:00:00 2001 From: Hanks Date: Wed, 27 Dec 2017 10:33:37 +0800 Subject: [PATCH] feat(weex): support parse object literal in binding attrs and styles (#7291) --- package.json | 2 + packages/weex-template-compiler/package.json | 2 + .../compiler/modules/recycle-list/v-bind.js | 5 +- src/platforms/weex/compiler/modules/style.js | 10 +- src/platforms/weex/util/parser.js | 60 ++++++++++ test/weex/cases/cases.spec.js | 2 +- test/weex/compiler/parser.spec.js | 105 ++++++++++++++++++ 7 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 src/platforms/weex/util/parser.js create mode 100644 test/weex/compiler/parser.spec.js diff --git a/package.json b/package.json index 70a1447ca8e..eccfa22f650 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "devDependencies": { "@types/node": "^8.0.33", "@types/webpack": "^3.0.13", + "acorn": "^5.2.1", "babel-core": "^6.25.0", "babel-eslint": "^8.0.3", "babel-helper-vue-jsx-merge-props": "^2.0.2", @@ -79,6 +80,7 @@ "cz-conventional-changelog": "^2.0.0", "de-indent": "^1.0.2", "es6-promise": "^4.1.0", + "escodegen": "^1.8.1", "eslint": "^4.13.1", "eslint-loader": "^1.7.1", "eslint-plugin-flowtype": "^2.34.0", diff --git a/packages/weex-template-compiler/package.json b/packages/weex-template-compiler/package.json index 19c493a216e..ad08235d07f 100644 --- a/packages/weex-template-compiler/package.json +++ b/packages/weex-template-compiler/package.json @@ -18,6 +18,8 @@ }, "homepage": "https://github.com/vuejs/vue/tree/dev/packages/weex-template-compiler#readme", "dependencies": { + "acorn": "^5.2.1", + "escodegen": "^1.8.1", "he": "^1.1.0" } } diff --git a/src/platforms/weex/compiler/modules/recycle-list/v-bind.js b/src/platforms/weex/compiler/modules/recycle-list/v-bind.js index b8d05a2c628..effdf351126 100644 --- a/src/platforms/weex/compiler/modules/recycle-list/v-bind.js +++ b/src/platforms/weex/compiler/modules/recycle-list/v-bind.js @@ -1,6 +1,7 @@ /* @flow */ import { camelize } from 'shared/util' +import { generateBinding } from 'weex/util/parser' import { bindRE } from 'compiler/parser/index' import { getAndRemoveAttr, addRawAttr } from 'compiler/helpers' @@ -12,9 +13,7 @@ export function preTransformVBind (el: ASTElement, options: WeexCompilerOptions) for (const attr in el.attrsMap) { if (bindRE.test(attr)) { const name: string = parseAttrName(attr) - const value = { - '@binding': getAndRemoveAttr(el, attr) - } + const value = generateBinding(getAndRemoveAttr(el, attr)) delete el.attrsMap[attr] addRawAttr(el, name, value) } diff --git a/src/platforms/weex/compiler/modules/style.js b/src/platforms/weex/compiler/modules/style.js index eb3c93a067a..7bf5027fa39 100644 --- a/src/platforms/weex/compiler/modules/style.js +++ b/src/platforms/weex/compiler/modules/style.js @@ -1,6 +1,6 @@ /* @flow */ -import { cached, camelize } from 'shared/util' +import { cached, camelize, isPlainObject } from 'shared/util' import { parseText } from 'compiler/parser/text-parser' import { getAndRemoveAttr, @@ -10,7 +10,7 @@ import { type StaticStyleResult = { dynamic: boolean, - styleResult: string + styleResult: string | Object | void }; const normalize = cached(camelize) @@ -27,12 +27,14 @@ function transformNode (el: ASTElement, options: CompilerOptions) { ) } if (!dynamic && styleResult) { + // $flow-disable-line el.staticStyle = styleResult } const styleBinding = getBindingAttr(el, 'style', false /* getStatic */) if (styleBinding) { el.styleBinding = styleBinding } else if (dynamic) { + // $flow-disable-line el.styleBinding = styleResult } } @@ -53,7 +55,7 @@ function parseStaticStyle (staticStyle: ?string, options: CompilerOptions): Stat // "width: 200px; height: {{y}}" -> {width: 200, height: y} let dynamic = false let styleResult = '' - if (staticStyle) { + if (typeof staticStyle === 'string') { const styleList = staticStyle.trim().split(';').map(style => { const result = style.trim().split(':') if (result.length !== 2) { @@ -71,6 +73,8 @@ function parseStaticStyle (staticStyle: ?string, options: CompilerOptions): Stat if (styleList.length) { styleResult = '{' + styleList.join(',') + '}' } + } else if (isPlainObject(staticStyle)) { + styleResult = JSON.stringify(staticStyle) || '' } return { dynamic, styleResult } } diff --git a/src/platforms/weex/util/parser.js b/src/platforms/weex/util/parser.js new file mode 100644 index 00000000000..081908e82b4 --- /dev/null +++ b/src/platforms/weex/util/parser.js @@ -0,0 +1,60 @@ +/* @flow */ + +// import { warn } from 'core/util/index' + +// this will be preserved during build +// $flow-disable-line +const acorn = require('acorn') // $flow-disable-line +const walk = require('acorn/dist/walk') // $flow-disable-line +const escodegen = require('escodegen') + +export function nodeToBinding (node: Object): any { + switch (node.type) { + case 'Literal': return node.value + case 'Identifier': + case 'UnaryExpression': + case 'BinaryExpression': + case 'LogicalExpression': + case 'ConditionalExpression': + case 'MemberExpression': return { '@binding': escodegen.generate(node) } + case 'ArrayExpression': return node.elements.map(_ => nodeToBinding(_)) + case 'ObjectExpression': { + const object = {} + node.properties.forEach(prop => { + if (!prop.key || prop.key.type !== 'Identifier') { + return + } + const key = escodegen.generate(prop.key) + const value = nodeToBinding(prop.value) + if (key && value) { + object[key] = value + } + }) + return object + } + default: { + // warn(`Not support ${node.type}: "${escodegen.generate(node)}"`) + return '' + } + } +} + +export function generateBinding (exp: ?string): any { + if (exp && typeof exp === 'string') { + let ast = null + try { + ast = acorn.parse(`(${exp})`) + } catch (e) { + // warn(`Failed to parse the expression: "${exp}"`) + return '' + } + + let output = '' + walk.simple(ast, { + Expression (node) { + output = nodeToBinding(node) + } + }) + return output + } +} diff --git a/test/weex/cases/cases.spec.js b/test/weex/cases/cases.spec.js index e5825ec7435..6d073e553eb 100644 --- a/test/weex/cases/cases.spec.js +++ b/test/weex/cases/cases.spec.js @@ -64,7 +64,7 @@ describe('Usage', () => { it('text node', createRenderTestCase('recycle-list/text-node')) it('attributes', createRenderTestCase('recycle-list/attrs')) // it('class name', createRenderTestCase('recycle-list/classname')) - // it('inline style', createRenderTestCase('recycle-list/inline-style')) + it('inline style', createRenderTestCase('recycle-list/inline-style')) it('v-if', createRenderTestCase('recycle-list/v-if')) it('v-else', createRenderTestCase('recycle-list/v-else')) it('v-else-if', createRenderTestCase('recycle-list/v-else-if')) diff --git a/test/weex/compiler/parser.spec.js b/test/weex/compiler/parser.spec.js new file mode 100644 index 00000000000..1d560b5b44c --- /dev/null +++ b/test/weex/compiler/parser.spec.js @@ -0,0 +1,105 @@ +import { generateBinding } from '../../../src/platforms/weex/util/parser' + +describe('expression parser', () => { + describe('generateBinding', () => { + it('primitive literal', () => { + expect(generateBinding('15')).toEqual(15) + expect(generateBinding('"xxx"')).toEqual('xxx') + }) + + it('identifiers', () => { + expect(generateBinding('x')).toEqual({ '@binding': 'x' }) + expect(generateBinding('x.y')).toEqual({ '@binding': 'x.y' }) + expect(generateBinding(`x.y['z']`)).toEqual({ '@binding': `x.y['z']` }) + }) + + it('object literal', () => { + expect(generateBinding('{}')).toEqual({}) + expect(generateBinding('{ abc: 25 }')).toEqual({ abc: 25 }) + expect(generateBinding('{ abc: 25, def: "xxx" }')).toEqual({ abc: 25, def: 'xxx' }) + expect(generateBinding('{ a: 3, b: { bb: "bb", bbb: { bbc: "BBC" } } }')) + .toEqual({ a: 3, b: { bb: 'bb', bbb: { bbc: 'BBC' }}}) + }) + + it('array literal', () => { + expect(generateBinding('[]')).toEqual([]) + expect(generateBinding('[{ abc: 25 }]')).toEqual([{ abc: 25 }]) + expect(generateBinding('[{ abc: 25, def: ["xxx"] }]')).toEqual([{ abc: 25, def: ['xxx'] }]) + expect(generateBinding('{ a: [3,16], b: [{ bb: ["aa","bb"], bbb: [{bbc:"BBC"}] }] }')) + .toEqual({ a: [3, 16], b: [{ bb: ['aa', 'bb'], bbb: [{ bbc: 'BBC' }] }] }) + }) + + it('expressions', () => { + expect(generateBinding(`3 + 5`)).toEqual({ '@binding': `3 + 5` }) + expect(generateBinding(`'x' + 2`)).toEqual({ '@binding': `'x' + 2` }) + expect(generateBinding(`\`xx\` + 2`)).toEqual({ '@binding': `\`xx\` + 2` }) + expect(generateBinding(`item.size * 23 + 'px'`)).toEqual({ '@binding': `item.size * 23 + 'px'` }) + }) + + it('object bindings', () => { + expect(generateBinding(`{ color: textColor }`)).toEqual({ + color: { '@binding': 'textColor' } + }) + expect(generateBinding(`{ color: '#FF' + 66 * 100, fontSize: item.size }`)).toEqual({ + color: { '@binding': `'#FF' + 66 * 100` }, + fontSize: { '@binding': 'item.size' } + }) + expect(generateBinding(`{ + x: { xx: obj, xy: -2 + 5 }, + y: { + yy: { yyy: obj.y || yy }, + yz: typeof object.yz === 'string' ? object.yz : '' + } + }`)).toEqual({ + x: { xx: { '@binding': 'obj' }, xy: { '@binding': '-2 + 5' }}, + y: { + yy: { yyy: { '@binding': 'obj.y || yy' }}, + yz: { '@binding': `typeof object.yz === 'string' ? object.yz : ''` } + } + }) + }) + + it('array bindings', () => { + expect(generateBinding(`[textColor, 3 + 5, 'string']`)).toEqual([ + { '@binding': 'textColor' }, + { '@binding': '3 + 5' }, + 'string' + ]) + expect(generateBinding(`[ + { color: '#FF' + 66 * -100 }, + item && item.style, + { fontSize: item.size | 0 } + ]`)).toEqual([ + { color: { '@binding': `'#FF' + 66 * -100` }}, + { '@binding': 'item && item.style' }, + { fontSize: { '@binding': 'item.size | 0' }} + ]) + expect(generateBinding(`[{ + x: [{ xx: [fn instanceof Function ? 'function' : '' , 25] }], + y: { + yy: [{ yyy: [obj.yy.y, obj.y.yy] }], + yz: [object.yz, void 0] + } + }]`)).toEqual([{ + x: [{ xx: [{ '@binding': `fn instanceof Function ? 'function' : ''` }, 25] }], + y: { + yy: [{ yyy: [{ '@binding': 'obj.yy.y' }, { '@binding': 'obj.y.yy' }] }], + yz: [{ '@binding': 'object.yz' }, { '@binding': 'void 0' }] + } + }]) + }) + + it('unsupported bindings', () => { + expect(generateBinding('() => {}')).toEqual('') + expect(generateBinding('function(){}')).toEqual('') + expect(generateBinding('(function(){})()')).toEqual('') + expect(generateBinding('var abc = 35')).toEqual('') + expect(generateBinding('abc++')).toEqual('') + expect(generateBinding('x.y(0)')).toEqual('') + expect(generateBinding('class X {}')).toEqual('') + expect(generateBinding('if (typeof x == null) { 35 }')).toEqual('') + expect(generateBinding('while (x == null)')).toEqual('') + expect(generateBinding('new Function()')).toEqual('') + }) + }) +})