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
+ }
+]