diff --git a/package.json b/package.json index 1c5fc8d..7c46494 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "weex-vue-precompiler", - "version": "0.1.17", + "version": "0.1.18", "description": "a precompiler for weex-vue-render.", "main": "src/index.js", "scripts": { diff --git a/src/components/index.js b/src/components/index.js index b57bb40..3b52c76 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,7 +1,21 @@ +const div = require('./div') +const image = require('./image') +const text = require('./text') +const a = require('./a') +const cell = require('./cell') + +const cmpMaps = { div, image, text, a, cell } + module.exports = { - div: require('./div').processDiv, - figure: require('./image').processImage, - p: require('./text').processText, - a: require('./a').processA, - section: require('./cell').processCell + div: div.processDiv, + figure: image.processImage, + p: text.processText, + a: a.processA, + section: cell.processCell, + // get ast compiler for binding styles. + getCompiler: function (tag) { + const cmp = cmpMaps[tag] + const compile = cmp && cmp.compile + return compile ? { compile } : undefined + } } diff --git a/src/components/text.js b/src/components/text.js index c54df82..24e9a18 100644 --- a/src/components/text.js +++ b/src/components/text.js @@ -1,5 +1,6 @@ const util = require('../util') const { + ast, extend, getStaticStyleObject } = util @@ -43,3 +44,31 @@ exports.processText = function ( delete el.ns el.plain = false } + +// deal with binding-styles ast node. +exports.compile = function (objNode, px2remTags, rootValue, transformNode) { + const props = objNode.properties + let hasLines = false + for (let i = 0, l = props.length; i < l; i++) { + const propNode = props[i] + const keyNode = propNode.key + const keyType = keyNode.type + const keyNodeValStr = keyType === 'Literal' ? 'value' : 'name' + const keyName = keyNode[keyNodeValStr] + const valNode = propNode.value + if (keyName === 'lines') { + hasLines = true + keyNode[keyNodeValStr] = 'webkitLineClamp' + } + else if (px2remTags.indexOf(keyName) > -1) { + propNode.value = transformNode(propNode.value, 'text', rootValue, true/*asPropValue*/) + } + } + if (hasLines) { + objNode.properties = props.concat([ + ast.genPropNode('overflow', 'hidden'), + ast.genPropNode('textOverflow', 'ellipsis') + ]) + } + return objNode +} diff --git a/src/config.js b/src/config.js index 6de8244..e771bb7 100644 --- a/src/config.js +++ b/src/config.js @@ -1,3 +1,20 @@ +const util = require('./util') + +const vendorReg = /webkit|moz/i +function hyphen (key) { + return util.hyphenate(key.replace(vendorReg, function ($0) { + return `-${$0.toLowerCase()}-` + })) +} + +function getAllStyles (scaleStyles) { + return Object.keys(scaleStyles.reduce(function (pre, key) { + pre[key] = 1 + pre[hyphen(key)] = 1 + return pre + }, {})) +} + const config = { eventMap: { click: 'weex$tap', @@ -81,27 +98,6 @@ const config = { 'finish', 'fail' ], - preservedTags: [ - 'a', - 'container', - 'div', - 'image', - 'img', - 'text', - 'input', - 'switch', - 'list', - 'scroller', - 'waterfall', - 'slider', - 'indicator', - 'loading-indicator', - 'loading', - 'refresh', - 'textarea', - 'video', - 'web' - ], autoprefixer: { browsers: ['> 0.1%', 'ios >= 8', 'not ie < 12'] }, @@ -109,7 +105,7 @@ const config = { rootValue: 75, minPixelValue: 1.01 }, - bindingStyleNamesForPx2Rem: [ + bindingStyleNamesForPx2Rem: getAllStyles([ 'width', 'height', 'left', @@ -145,7 +141,7 @@ const config = { 'mozTransform', 'MozTransform', 'itemSize' - ] + ]) } module.exports = config diff --git a/src/hooks/style-binding.js b/src/hooks/style-binding.js index 073a0ef..5541e32 100644 --- a/src/hooks/style-binding.js +++ b/src/hooks/style-binding.js @@ -5,68 +5,122 @@ const esprima = require('esprima') const escodegen = require('escodegen') const bindingStyleNamesForPx2Rem = require('../config').bindingStyleNamesForPx2Rem +const issues = 'https://github.com/weexteam/weex-vue-precompiler/issues' -const { parseAst } = require('../util') -const { getCompiler, getTransformer } = require('wxv-transformer') +const { ast } = require('../util') +const { getCompiler } = require('../components') +const { getTransformer } = require('wxv-transformer') -function alreadyTransformed (node) { - if (node - && node.type === 'CallExpression' - && node.callee - && (node.callee.name + ''.match(/_processExclusiveStyle|_px2rem/))) - { - return true +function transformArray (ast, tagName, rootValue) { + const elements = ast.elements + for (let i = 0, l = elements.length; i < l; i++) { + const element = elements[i] + const result = transformNode(element, tagName, rootValue) + if (result) { + elements[i] = result + } } - return false + return ast +} + +/** + * transform ConditionalExpressions. e.g.: + * :style="a ? b : c" => :style="_px2rem(a, rootValue) ? _px2rem(b, rootValue) : _px2rem(c, rootValue)" + * @param {ConditionalExpression} ast + */ +function transformConditional (ast, tagName, rootValue) { + ast.consequent = transformNode(ast.consequent, tagName, rootValue) + ast.alternate = transformNode(ast.alternate, tagName, rootValue) + return ast } /** * transform :style="{width:w}" => :style="{width:_px2rem(w, rootValue)}" * This kind of style binding with object literal is a good practice. - * @param {ObjectExpression} obj + * @param {ObjectExpression} ast */ -function transformObject (obj, origTagName, rootValue) { - const compiler = getCompiler(origTagName) +function transformObject (ast, tagName, rootValue) { + const compiler = getCompiler(tagName) if (compiler) { - return compiler.compile(obj, bindingStyleNamesForPx2Rem, rootValue) + return compiler.compile(ast, bindingStyleNamesForPx2Rem, rootValue, transformNode) } - const properties = obj.properties + const properties = ast.properties for (let i = 0, l = properties.length; i < l; i++) { const prop = properties[i] const keyNode = prop.key const keyType = keyNode.type const key = keyType === 'Literal' ? keyNode.value : keyNode.name - const valNode = prop.value - if (alreadyTransformed(valNode)) { - continue - } if (bindingStyleNamesForPx2Rem.indexOf(key) > -1) { - prop.value = { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: '_px2rem' - }, - arguments: [valNode, { type: 'Literal', value: rootValue }] - } + prop.value = transformNode(prop.value, tagName, rootValue, true/*asPropValue*/) } } + return ast +} + +function transformLiteral (...args) { + // not to transform literal string directly since we don't know + // if we could use 0.5px to support hairline unless in runtime. + return transformAsWhole(...args) +} + +/** + * type = 'Identifier' + * @param {Identifier} ast + */ +function transformIdentifier (...args) { + return transformAsWhole(...args) +} + +/** + * transform MemberExpression like :styles="myData.styles" + */ +function transformMember (...args) { + return transformAsWhole(...args) } /** - * transform :style="someObj" => :style="_px2rem(someObj, opts)" - * This kind of binding with object variable could cause runtime - * performance reducing. - * @param {Identifier} node - * @param {string} tagName + * transform CallExpression like :stylles="getMyStyles()" */ -function transformVariable (node, tagName, rootValue) { - if (alreadyTransformed(node)) { - return node +function transformCall (ast, ...args) { + const name = ast.callee.name + if (name && name.match(/_processExclusiveStyle|_px2rem/)) { + return ast // already transformed. } + return transformAsWhole(ast, ...args) +} +/** + * transform a value object for a property in a object expression. + * @param {Literal || Identifier} ast a value expression in object literal. + */ +function transformAsValue (ast, tagName, rootValue) { + return { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: '_px2rem' + }, + arguments: [ast, { type: 'Literal', value: rootValue }] + } +} + +/** + * transform :style="expression" => :style="_px2rem(expression, opts)" directly + * wrapping with _px2rem or _processExclusiveStyle function. + * ////////////////////// + * support node type: + * - MemberExpression + * - Identifier + * - CallExpression + * ////////////////////// + * not support: + * - ObjectExpression + * - ConditionalExpression + * - ArrayExpression + */ +function transformAsWhole (ast, tagName, rootValue) { let callName = '_px2rem' - const args = [node, { type: 'Literal', value: rootValue }] + const args = [ast, { type: 'Literal', value: rootValue }] const transformer = getTransformer(tagName) if (transformer) { // special treatment for exclusive styles, such as text-lines @@ -86,6 +140,40 @@ function transformVariable (node, tagName, rootValue) { } } +/** + * @param {boolean} asPropValue: whether this ast node is a value node for a style + * object. If it is, we shouldn't use _processExclusiveStyle. + */ +function transformNode (ast, tagName, rootValue, asPropValue) { + if (asPropValue) { + return transformAsValue(ast, tagName, rootValue) + } + const type = ast.type + switch (type) { + // not as whole types. + case 'ArrayExpression': + return transformArray(ast, tagName, rootValue) + case 'ConditionalExpression': + return transformConditional(ast, tagName, rootValue) + case 'ObjectExpression': + return transformObject(ast, tagName, rootValue) + // as whole types. + case 'Identifier': + return transformIdentifier(ast, tagName, rootValue) + case 'CallExpression': + return transformCall(ast, tagName, rootValue) + case 'MemberExpression': + return transformMember(ast, tagName, rootValue) + case 'Literal': + return transformLiteral(ast, tagName, rootValue) + default: { + console.warn('[weex-vue-precompiler]: current expression not in transform lists:', type) + console.log('[weex-vue-precomiler]: current ast node:', ast) + return transformAsWhole(ast, tagName, rootValue) + } + } +} + function styleBindingHook ( el, attrsMap, @@ -93,50 +181,27 @@ function styleBindingHook ( attrs, staticClass ) { - const styleBinding = el.styleBinding - if (!styleBinding) { - return - } - let ast = parseAst(styleBinding.trim()) - const { rootValue } = this.config.px2rem - if (ast.type === 'ArrayExpression') { - const elements = ast.elements - for (let i = 0, l = elements.length; i < l; i++) { - const element = elements[i] - if (element.type === 'ObjectExpression') { - transformObject(element, el._origTag || el.tag, rootValue) - } - /** - * otherwise element.type === - * - 'Identifier': varaibles - * - 'MemberExpression': member of varaibles - */ - else { - elements[i] = transformVariable(element, el._origTag || el.tag, rootValue) - } + try { + const styleBinding = el.styleBinding + if (!styleBinding) { + return } + const parsedAst = ast.parseAst(styleBinding.trim()) + const { rootValue } = this.config.px2rem + const transformedAst = transformNode(parsedAst, el._origTag || el.tag, rootValue) + const res = escodegen.generate(transformedAst, { + format: { + indent: { + style: ' ' + }, + newline: '', + } + }) + el.styleBinding = res + } catch (err) { + console.log(`[weex-vue-precompiler] ooops! There\'s a err, please paste this error` + + `stack to the repo's issue list: ${issues}`, err) } - else if (ast.type === 'ObjectExpression') { - transformObject(ast, el._origTag || el.tag, rootValue) - } - else { - /** - * ast.type === - * - Identifier (varaible) - * - MemberExpression (somObj.somProp) - */ - ast = transformVariable(ast, el._origTag || el.tag, rootValue) - } - const res = escodegen.generate(ast, { - format: { - indent: { - style: ' ' - }, - newline: '', - } - }) - // console.log('res:', res) - el.styleBinding = res } module.exports = styleBindingHook diff --git a/src/hooks/test.js b/src/hooks/test.js new file mode 100644 index 0000000..9ca25e5 --- /dev/null +++ b/src/hooks/test.js @@ -0,0 +1,48 @@ +function genPropNode (k, v) { + return { + type: 'Property', + key: { + type: 'Identifier', + name: k + }, + computed: false, + value: { + type: 'Literal', + value: v + }, + kind: 'init', + method: false, + shorthand: false + } +} + +module.exports = function () { + return { + compile (objNode, px2remTags, rootValue, transformNode) { + const props = objNode.properties + let hasLines = false + for (let i = 0, l = props.length; i < l; i++) { + const propNode = props[i] + const keyNode = propNode.key + const keyType = keyNode.type + const keyNodeValStr = keyType === 'Literal' ? 'value' : 'name' + const keyName = keyNode[keyNodeValStr] + const valNode = propNode.value + if (keyName === 'lines') { + hasLines = true + keyNode[keyNodeValStr] = 'webkitLineClamp' + } + else if (px2remTags.indexOf(keyName) > -1) { + propNode.value = transformNode(propNode.value, 'text', rootValue, true/*asPropValue*/) + } + } + if (hasLines) { + objNode.properties = props.concat([ + genPropNode('overflow', 'hidden'), + genPropNode('textOverflow', 'ellipsis') + ]) + } + return objNode + } + } +} diff --git a/src/index.js b/src/index.js index 662cff4..8826be0 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,50 @@ const hooks = require('./hooks') const util = require('./util') const defaults = require('./config') +const arrayMergeOpts = [ + 'weexRegisteredComponents', + 'weexBuiltInComponents', + 'aliweexComponents' +] + +function mergeConfig(thisConfig, config) { + if (config) { + const { + weexRegisteredComponents, + weexBuiltInComponents, + aliweex, + aliweexComponents + } = config + + // merge all fields except arrays. + for (const k in config) { + if (arrayMergeOpts.indexOf(k) <= -1) { + thisConfig[k] = config[k] + } + } + + // merge array fields. + arrayMergeOpts.forEach(function (optName) { + const vals = config[optName] + if (util.isArray(vals)) { + thisConfig[optName] = util.mergeStringArray( + thisConfig[optName], + vals + ) + } + }) + } + + // At last, set the preservedTags. + thisConfig.preservedTags = thisConfig.weexRegisteredComponents + .concat(thisConfig.weexBuiltInComponents) + if (config && config.aliweex) { + thisConfig.preservedTags = thisConfig.preservedTags.concat( + thisConfig.aliweexComponents + ) + } +} + class Precompiler { /** * config: @@ -20,7 +64,7 @@ class Precompiler { */ constructor (config) { this.config = defaults - util.extend(this.config, config) + mergeConfig(this.config, config) } precompile (el) { diff --git a/src/util/ast.js b/src/util/ast.js new file mode 100644 index 0000000..a417edb --- /dev/null +++ b/src/util/ast.js @@ -0,0 +1,40 @@ +const esprima = require('esprima') + +exports.genPropNode = function (k, v) { + return { + type: 'Property', + key: { + type: 'Identifier', + name: k + }, + computed: false, + value: { + type: 'Literal', + value: v + }, + kind: 'init', + method: false, + shorthand: false + } +} + +exports.parseAst = (function () { + const _cached = {} + return function (val) { + let statement = 'a = ' + if (typeof val === 'object') { + statement += `${JSON.stringify(val)}` + } + else { + statement += val + } + const cached = _cached[statement] + if (cached) { + return cached + } + const ast = esprima.parse(statement) + .body[0].expression.right + _cached[statement] = ast + return ast + } +})() diff --git a/src/util/index.js b/src/util/index.js index d2b58f2..9b9afb0 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -1,4 +1,5 @@ -const esprima = require('esprima') +const ast = require('./ast') +exports.ast = ast exports.extend = function (to, from) { if (!to) { return } @@ -15,6 +16,11 @@ exports.extend = function (to, from) { return to } +const ARRAY_STRING = '[object Array]' +exports.isArray = function (arr) { + return toString.call(arr) === ARRAY_STRING +} + const camelizeRE = /-(\w)/g exports.camelize = str => { return str.replace(camelizeRE, (_, c) => c.toUpperCase()) @@ -36,22 +42,14 @@ exports.getStaticStyleObject = function (el) { return staticStyle || {} } -const parseAst = function parseAst (val) { - let statement = 'a = ' - if (typeof val === 'object') { - statement += `${JSON.stringify(val)}` - } - else { - statement += val - } - const cached = parseAst._cached[statement] - if (cached) { - return cached +exports.mergeStringArray = function () { + const arrs = Array.prototype.slice.call(arguments) + const arrMap = {} + for (let i = 0, l = arrs.length; i < l; i++) { + const arr = arrs[i] + for (let j = 0, len = arr.length; j < len; j++) { + arrMap[arr[j]] = true + } } - const ast = esprima.parse(statement) - .body[0].expression.right - parseAst._cached[statement] = ast - return ast + return Object.keys(arrMap) } -parseAst._cached = {} -exports.parseAst = parseAst diff --git a/test/input/components/image.vue b/test/input/components/image.vue index f791eb4..1eebc38 100644 --- a/test/input/components/image.vue +++ b/test/input/components/image.vue @@ -4,6 +4,6 @@ src="#src" placeholder="#placeholder" :resize="resize" - class="img"> + class="img" /> diff --git a/test/input/components/list.vue b/test/input/components/list.vue new file mode 100644 index 0000000..0ece9bd --- /dev/null +++ b/test/input/components/list.vue @@ -0,0 +1,3 @@ + diff --git a/test/input/hooks/style-binding.vue b/test/input/hooks/style-binding.vue new file mode 100644 index 0000000..2cd74be --- /dev/null +++ b/test/input/hooks/style-binding.vue @@ -0,0 +1,11 @@ + diff --git a/test/output/components/list.js b/test/output/components/list.js new file mode 100644 index 0000000..1dc475b --- /dev/null +++ b/test/output/components/list.js @@ -0,0 +1,25 @@ +module.exports = [ + { + type: 1, + tag: 'list', + _hasBubbleParent: false, + _weexRegistered: true, + plain: false, + hasBindings: true, + attrs: [ + { + name: 'data-evt-scroll', + value: '""' + } + ], + nativeEvents: { + 'weex$scroll': { + modifiers: { + stop: true + }, + value: 'scroll' + } + }, + static: false + } +] diff --git a/test/output/hooks/style-binding.js b/test/output/hooks/style-binding.js new file mode 100644 index 0000000..3e3e071 --- /dev/null +++ b/test/output/hooks/style-binding.js @@ -0,0 +1,117 @@ +const { extend } = require('../../../src/util') + +module.exports = [ + { + type: 1, + tag: 'p', + _origTag: 'text', + _weexBuiltIn: true, + plain: false, + attrs: [ + { + name: 'weex-type', + value: '"text"' + } + ], + staticClass: '" weex-el weex-text"', + styleBinding: "{ 'margin-left': _px2rem('20px', 75), 'marginBottom': _px2rem('10px', 75)}", + }, { + type: 1, + tag: 'p', + _origTag: 'text', + _weexBuiltIn: true, + plain: false, + attrs: [ + { + name: 'weex-type', + value: '"text"' + } + ], + staticClass: '" weex-el weex-text"', + styleBinding: "{ 'margin-left': _px2rem(myMargin + 'px', 75) }", + }, { + type: 1, + tag: 'p', + _origTag: 'text', + _weexBuiltIn: true, + plain: false, + attrs: [ + { + name: 'weex-type', + value: '"text"' + } + ], + staticClass: '" weex-el weex-text"', + styleBinding: "_processExclusiveStyle(myData.styles, 75, 'text')", + }, { + type: 1, + tag: 'div', + _origTag: 'div', + _weexBuiltIn: true, + plain: false, + attrs: [ + { + name: 'weex-type', + value: '"div"' + } + ], + staticClass: '" weex-ct weex-div"', + styleBinding: "_px2rem(myData.styles, 75)", + }, { + type: 1, + tag: 'div', + _origTag: 'div', + _weexBuiltIn: true, + plain: false, + attrs: [ + { + name: 'weex-type', + value: '"div"' + } + ], + staticClass: '" weex-ct weex-div"', + styleBinding: "_px2rem(getStyles('div'), 75)", + }, { + type: 1, + tag: 'div', + _origTag: 'div', + _weexBuiltIn: true, + plain: false, + attrs: [ + { + name: 'weex-type', + value: '"div"' + } + ], + staticClass: '" weex-ct weex-div"', + styleBinding: "isMyTheme ? { marginLeft: _px2rem('20px', 75) } : _px2rem(yourStyles, 75)", + }, { + type: 1, + tag: 'div', + _origTag: 'div', + _weexBuiltIn: true, + plain: false, + attrs: [ + { + name: 'weex-type', + value: '"div"' + } + ], + staticClass: '" weex-ct weex-div"', + styleBinding: "isMyTheme ? _px2rem(myStyles, 75) : _px2rem(yourStyles, 75)", + }, { + type: 1, + tag: 'div', + _origTag: 'div', + _weexBuiltIn: true, + plain: false, + attrs: [ + { + name: 'weex-type', + value: '"div"' + } + ], + staticClass: '" weex-ct weex-div"', + static: false + } +]