├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── LICENSE ├── README.md ├── build ├── alias.js ├── build.js ├── ci.sh ├── config.js ├── gen-release-note.js ├── git-hooks │ ├── commit-msg │ └── pre-commit ├── install-hooks.js └── release.sh ├── circle.yml ├── docs ├── implementation.md └── resources │ ├── C2DA06C0F8A4D40C55ADF4BB8D3312B4.jpg │ └── D26E6CDB0A8FD3838D3BB09ECF22727C.jpg ├── flow ├── compiler.js ├── component.js ├── global-api.js ├── modules.js ├── options.js ├── ssr.js └── vnode.js ├── package-lock.json ├── package.json ├── packages └── wxml-transpiler │ ├── README.md │ ├── index.js │ └── package.json ├── publish.sh ├── src ├── compiler │ ├── codegen │ │ ├── events.js │ │ ├── index.js │ │ └── template.js │ ├── create-compiler.js │ ├── directives │ │ ├── bind.js │ │ ├── index.js │ │ ├── model.js │ │ └── on.js │ ├── error-detector.js │ ├── helpers.js │ ├── index.js │ ├── optimizer.js │ ├── parser │ │ ├── entity-decoder.js │ │ ├── filter-parser.js │ │ ├── html-parser.js │ │ ├── index.js │ │ └── text-parser.js │ └── to-function.js ├── core │ ├── components │ │ ├── index.js │ │ └── keep-alive.js │ ├── config.js │ ├── global-api │ │ ├── assets.js │ │ ├── extend.js │ │ ├── index.js │ │ ├── mixin.js │ │ └── use.js │ ├── index.js │ ├── instance │ │ ├── events.js │ │ ├── index.js │ │ ├── init.js │ │ ├── inject.js │ │ ├── lifecycle.js │ │ ├── proxy.js │ │ ├── render-helpers │ │ │ ├── bind-object-listeners.js │ │ │ ├── bind-object-props.js │ │ │ ├── check-keycodes.js │ │ │ ├── render-list.js │ │ │ ├── render-slot.js │ │ │ ├── render-static.js │ │ │ ├── resolve-filter.js │ │ │ └── resolve-slots.js │ │ ├── render.js │ │ └── state.js │ ├── observer │ │ ├── array.js │ │ ├── dep.js │ │ ├── index.js │ │ ├── scheduler.js │ │ └── watcher.js │ ├── util │ │ ├── debug.js │ │ ├── env.js │ │ ├── error.js │ │ ├── index.js │ │ ├── lang.js │ │ ├── options.js │ │ ├── perf.js │ │ └── props.js │ └── vdom │ │ ├── create-component.js │ │ ├── create-element.js │ │ ├── create-functional-component.js │ │ ├── helpers │ │ ├── extract-props.js │ │ ├── get-first-component-child.js │ │ ├── index.js │ │ ├── merge-hook.js │ │ ├── normalize-children.js │ │ ├── resolve-async-component.js │ │ └── update-listeners.js │ │ ├── modules │ │ ├── directives.js │ │ ├── index.js │ │ └── ref.js │ │ ├── patch.js │ │ └── vnode.js ├── platforms │ └── web │ │ ├── compiler │ │ ├── directives │ │ │ ├── html.js │ │ │ ├── index.js │ │ │ ├── model.js │ │ │ └── text.js │ │ ├── index.js │ │ ├── modules │ │ │ ├── class.js │ │ │ ├── index.js │ │ │ └── style.js │ │ ├── options.js │ │ └── util.js │ │ ├── entry-compiler.js │ │ └── util │ │ ├── attrs.js │ │ ├── class.js │ │ ├── compat.js │ │ ├── element.js │ │ ├── index.js │ │ └── style.js ├── sfc │ └── parser.js └── shared │ ├── constants.js │ └── util.js └── test ├── lib ├── wcc └── wcc-new ├── pages ├── full │ └── index.wxml ├── index │ └── index.wxml └── logs │ └── logs.wxml └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "flow-vue"], 3 | "plugins": ["transform-vue-jsx", "syntax-dynamic-import"], 4 | "ignore": [ 5 | "dist/*.js", 6 | "packages/**/*.js" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | flow 2 | dist 3 | packages 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "plugins": [ 4 | "flowtype" 5 | ], 6 | "extends": [ 7 | "plugin:vue-libs/recommended", 8 | "plugin:flowtype/recommended" 9 | ], 10 | "globals": { 11 | "__WEEX__": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.* 3 | .*/test/.* 4 | .*/build/.* 5 | .*/examples/.* 6 | .*/benchmarks/.* 7 | 8 | [include] 9 | 10 | [libs] 11 | flow 12 | 13 | [options] 14 | unsafe.enable_getters_and_setters=true 15 | module.name_mapper='^compiler/\(.*\)$' -> '/src/compiler/\1' 16 | module.name_mapper='^core/\(.*\)$' -> '/src/core/\1' 17 | module.name_mapper='^shared/\(.*\)$' -> '/src/shared/\1' 18 | module.name_mapper='^web/\(.*\)$' -> '/src/platforms/web/\1' 19 | module.name_mapper='^entries/\(.*\)$' -> '/src/entries/\1' 20 | module.name_mapper='^sfc/\(.*\)$' -> '/src/sfc/\1' 21 | suppress_comment= \\(.\\|\n\\)*\\$flow-disable-line 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.log 4 | *.diff 5 | explorations 6 | TODOs.md 7 | coverage 8 | RELEASE_NOTE*.md 9 | dist/*.js 10 | packages/wxml-transpiler/build.js 11 | test/dist 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-present, Yuxi (Evan) You 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wxml-transpiler 2 | 3 | ## Intro 4 | 5 | Port of wcc.cpp/wcc/wcc.exe to JavaScript: use compiler carried with Vue.js to transpile wxml ([Grammers to Support](https://mp.weixin.qq.com/debug/wxadoc/dev/framework/view/wxml/)). 6 | 7 | ## Give it a Try 8 | 9 | > Get Started 10 | 11 | ```sh 12 | # install deps 13 | npm i 14 | 15 | # build dep 16 | npm run build 17 | 18 | # run 19 | node test/test 20 | ``` 21 | 22 | > Dev Opts 23 | 24 | ```sh 25 | # auto rebuild 26 | npm run dev 27 | 28 | # autorestart type check system 29 | ## brew install watch 30 | watch -t npm run flow 31 | 32 | # autorestart test 33 | npm run autotest 34 | ``` 35 | 36 | ## Todo 37 | 38 | - error position feedback 39 | - `propStore` should better not be global 40 | - push props in parseText to reuse pushed props 41 | 42 | ## License 43 | 44 | [MIT](http://opensource.org/licenses/MIT) 45 | -------------------------------------------------------------------------------- /build/alias.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | compiler: path.resolve(__dirname, '../src/compiler'), 5 | core: path.resolve(__dirname, '../src/core'), 6 | shared: path.resolve(__dirname, '../src/shared'), 7 | web: path.resolve(__dirname, '../src/platforms/web'), 8 | entries: path.resolve(__dirname, '../src/entries'), 9 | sfc: path.resolve(__dirname, '../src/sfc') 10 | } 11 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const zlib = require('zlib') 4 | const rollup = require('rollup') 5 | const uglify = require('uglify-js') 6 | 7 | if (!fs.existsSync('dist')) { 8 | fs.mkdirSync('dist') 9 | } 10 | 11 | let builds = require('./config').getAllBuilds() 12 | 13 | // filter builds via command line arg 14 | if (process.argv[2]) { 15 | const filters = process.argv[2].split(',') 16 | builds = builds.filter(b => { 17 | return filters.some(f => b.dest.indexOf(f) > -1) 18 | }) 19 | } else { 20 | // filter out weex builds by default 21 | builds = builds.filter(b => { 22 | return b.dest.indexOf('weex') === -1 23 | }) 24 | } 25 | 26 | build(builds) 27 | 28 | function build (builds) { 29 | let built = 0 30 | const total = builds.length 31 | const next = () => { 32 | buildEntry(builds[built]).then(() => { 33 | built++ 34 | if (built < total) { 35 | next() 36 | } 37 | }).catch(logError) 38 | } 39 | 40 | next() 41 | } 42 | 43 | function buildEntry (config) { 44 | const isProd = true || /min\.js$/.test(config.dest) 45 | return rollup.rollup(config) 46 | .then(bundle => bundle.generate(config)) 47 | .then(({ code }) => { 48 | if (isProd) { 49 | var minified = (config.banner ? config.banner + '\n' : '') + uglify.minify(code, { 50 | output: { 51 | ascii_only: true 52 | }, 53 | compress: { 54 | pure_funcs: ['makeMap'] 55 | } 56 | }).code 57 | return write(config.dest, minified, true) 58 | } else { 59 | return write(config.dest, code) 60 | } 61 | }) 62 | } 63 | 64 | function write (dest, code, zip) { 65 | return new Promise((resolve, reject) => { 66 | function report (extra) { 67 | console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || '')) 68 | resolve() 69 | } 70 | 71 | fs.writeFile(dest, code, err => { 72 | if (err) return reject(err) 73 | if (zip) { 74 | zlib.gzip(code, (err, zipped) => { 75 | if (err) return reject(err) 76 | report(' (gzipped: ' + getSize(zipped) + ')') 77 | }) 78 | } else { 79 | report() 80 | } 81 | }) 82 | }) 83 | } 84 | 85 | function getSize (code) { 86 | return (code.length / 1024).toFixed(2) + 'kb' 87 | } 88 | 89 | function logError (e) { 90 | console.log(e) 91 | } 92 | 93 | function blue (str) { 94 | return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m' 95 | } 96 | -------------------------------------------------------------------------------- /build/ci.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | npm test 3 | 4 | # report coverage stats for non-PRs 5 | if [[ -z $CI_PULL_REQUEST ]]; then 6 | cat ./coverage/lcov.info | ./node_modules/.bin/codecov 7 | fi 8 | -------------------------------------------------------------------------------- /build/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const buble = require('rollup-plugin-buble') 3 | const alias = require('rollup-plugin-alias') 4 | const replace = require('rollup-plugin-replace') 5 | const flow = require('rollup-plugin-flow-no-whitespace') 6 | const version = process.env.VERSION || require('../package.json').version 7 | 8 | const aliases = require('./alias') 9 | const resolve = p => { 10 | const base = p.split('/')[0] 11 | if (aliases[base]) { 12 | return path.resolve(aliases[base], p.slice(base.length + 1)) 13 | } else { 14 | return path.resolve(__dirname, '../', p) 15 | } 16 | } 17 | 18 | const builds = { 19 | 'web-compiler': { 20 | entry: resolve('web/entry-compiler.js'), 21 | dest: resolve('packages/wxml-transpiler/build.js'), 22 | format: 'cjs', 23 | external: Object.keys(require('../packages/wxml-transpiler/package.json').dependencies) 24 | } 25 | } 26 | 27 | function genConfig (opts) { 28 | const config = { 29 | entry: opts.entry, 30 | dest: opts.dest, 31 | external: opts.external, 32 | format: opts.format, 33 | banner: opts.banner, 34 | moduleName: opts.moduleName || 'Vue', 35 | plugins: [ 36 | replace({ 37 | __WEEX__: !!opts.weex, 38 | __VERSION__: version 39 | }), 40 | flow(), 41 | buble(), 42 | alias(Object.assign({}, aliases, opts.alias)) 43 | ].concat(opts.plugins || []) 44 | } 45 | 46 | if (opts.env) { 47 | config.plugins.push(replace({ 48 | 'process.env.NODE_ENV': JSON.stringify(opts.env) 49 | })) 50 | } 51 | 52 | return config 53 | } 54 | 55 | if (process.env.TARGET) { 56 | module.exports = genConfig(builds[process.env.TARGET]) 57 | } else { 58 | exports.getBuild = name => genConfig(builds[name]) 59 | exports.getAllBuilds = () => Object.keys(builds).map(name => genConfig(builds[name])) 60 | } 61 | -------------------------------------------------------------------------------- /build/gen-release-note.js: -------------------------------------------------------------------------------- 1 | const version = process.argv[2] || process.env.VERSION 2 | const cc = require('conventional-changelog') 3 | const file = `./RELEASE_NOTE${version ? `_${version}` : ``}.md` 4 | const fileStream = require('fs').createWriteStream(file) 5 | 6 | cc({ 7 | preset: 'angular', 8 | pkg: { 9 | transform (pkg) { 10 | pkg.version = `v${version}` 11 | return pkg 12 | } 13 | } 14 | }).pipe(fileStream).on('close', () => { 15 | console.log(`Generated release note at ${file}`) 16 | }) 17 | -------------------------------------------------------------------------------- /build/git-hooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Validate commit log 4 | commit_regex='^Merge.+|(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .{1,50}' 5 | 6 | if ! grep -iqE "$commit_regex" "$1"; then 7 | echo 8 | echo " Error: proper commit message format is required for automated changelog generation." 9 | echo 10 | echo " - Use \`npm run commit\` to interactively generate a commit message." 11 | echo " - See .github/COMMIT_CONVENTION.md for more details." 12 | echo 13 | exit 1 14 | fi 15 | -------------------------------------------------------------------------------- /build/git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | files_to_lint=$(git diff --cached --name-only --diff-filter=ACM | grep '\.js$') 4 | 5 | if [ -n "$files_to_lint" ]; then 6 | NODE_ENV=production eslint --quiet $files_to_lint 7 | fi 8 | -------------------------------------------------------------------------------- /build/install-hooks.js: -------------------------------------------------------------------------------- 1 | const { test, ln, chmod } = require('shelljs') 2 | 3 | if (test('-e', '.git/hooks')) { 4 | ln('-sf', '../../build/git-hooks/pre-commit', '.git/hooks/pre-commit') 5 | chmod('+x', '.git/hooks/pre-commit') 6 | ln('-sf', '../../build/git-hooks/commit-msg', '.git/hooks/commit-msg') 7 | chmod('+x', '.git/hooks/commit-msg') 8 | } 9 | -------------------------------------------------------------------------------- /build/release.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | if [[ -z $1 ]]; then 4 | echo "Enter new version: " 5 | read VERSION 6 | else 7 | VERSION=$1 8 | fi 9 | 10 | read -p "Releasing $VERSION - are you sure? (y/n) " -n 1 -r 11 | echo 12 | if [[ $REPLY =~ ^[Yy]$ ]]; then 13 | echo "Releasing $VERSION ..." 14 | 15 | if [[ -z $SKIP_TESTS ]]; then 16 | npm run lint 17 | npm run flow 18 | npm run test:cover 19 | npm run test:e2e 20 | npm run test:ssr 21 | fi 22 | 23 | if [[ -z $SKIP_SAUCE ]]; then 24 | export SAUCE_BUILD_ID=$VERSION:`date +"%s"` 25 | npm run test:sauce 26 | fi 27 | 28 | # build 29 | VERSION=$VERSION npm run build 30 | 31 | # update packages 32 | cd packages/wxml-transpiler 33 | npm version $VERSION 34 | if [[ -z $RELEASE_TAG ]]; then 35 | npm publish 36 | else 37 | npm publish --tag $RELEASE_TAG 38 | fi 39 | cd - 40 | 41 | 42 | # commit 43 | git add -A 44 | git add -f \ 45 | packages/wxml-transpiler/build.js 46 | git commit -m "build: build $VERSION" 47 | npm version $VERSION --message "build: release $VERSION" 48 | 49 | # publish 50 | git push origin refs/tags/v$VERSION 51 | git push 52 | if [[ -z $RELEASE_TAG ]]; then 53 | npm publish 54 | else 55 | npm publish --tag $RELEASE_TAG 56 | fi 57 | fi 58 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6 4 | 5 | test: 6 | override: 7 | - bash build/ci.sh 8 | -------------------------------------------------------------------------------- /docs/implementation.md: -------------------------------------------------------------------------------- 1 | # 实现记录 2 | 3 | ## 目标 4 | 5 | 本项目的目标是实现wcc工具的大部分功能。该工具会遍历输入的文件列表,读取相应的文件,分别解析其中的组件(小程序中每一个标签即为一个组件),最后将它们组合起来输出一个JS文件。这个JS文件由三部分构成:首部定义了许多用于生成或组合虚拟节点的函数,中部存放所有组件的属性值,尾部将各个组件以函数组合的形式描述出来。 6 | 7 | 本项目主要完成的工作是该JS文件后两部分的生成工作。 8 | 9 | 简化后的示例: 10 | > 输入 11 | 12 | ```html 13 | 14 | ``` 15 | 16 | > 输出 17 | 18 | ```js 19 | ======== 顶部定义的函数 ======== 20 | function _createTagNode(tag) { // 根据组件名创建虚拟节点 21 | return { 22 | tag: tag 23 | attr: {}, 24 | children: [], 25 | n: [] 26 | } 27 | } 28 | function _pushChild(a, b) { // 父组件接收子组件 29 | b && a.children.push(b) 30 | } 31 | 32 | ======== 中部数据收集 ======== 33 | var z = [] 34 | function Z(ops) { 35 | z.push(ops) 36 | } 37 | Z([3, 'addNumberToFront']) 38 | Z([3, ' Add to the front ']) 39 | 40 | ======== 底部组件声明 ======== 41 | var A = _createTagNode('button') // 生成button组件 42 | _attachAttribute(A, 'bindtap', 0) // 绑定属性 !!!!!! -> 0代表z数组中的第一个元素 43 | var B = _createTextNode(1) // 生成子组件 !!!!!! -> 1代表z数组中的第一个元素 44 | _pushChild(A, B) // 父组件接收子组件 45 | _pushChild(rootNode, A) // 根组件接收父组件 46 | ``` 47 | 48 | ## 基础概念 49 | 50 | 转译器在现代前端开发过程中是一个必不可少的工具,如babel、uglifyjs以及postcss。这些工具的作用是将一门高级语言转换成同种或其它种类的高级语言,以拓展原有代码的功能或优化原有代码的性能。与传统的编译器或解释器不同,它并不需要生成与平台相关的二进制代码。 51 | 52 | 大部分转译器主要由以下三个部分构成: 53 | 54 | - 解析器(parser) 55 | - 代码优化器(optimizer)或代码转换器(transformer) 56 | - 代码生成器(generator) 57 | 58 | > 解释器 59 | 60 | 解析器会进行词法分析和语义分析。首先,词法分析负责将输入的代码文本拆分成一个个单词或算符(统称Token),如`12 * (3 + 4)^2`会被拆分为`12`、`*`、`(`、`3`、`+`、`4`、`)`、`^`和`2`。然后,语义分析器将这些Token组装成一颗语法树(AST == Abstract Syntax Tree),如下图: 61 | 62 | ![IMAGE](resources/D26E6CDB0A8FD3838D3BB09ECF22727C.jpg) 63 | 64 | > 代码转换器 65 | 66 | 代码转换器负责将原有的ast转换成我们期望的ast,以上例,如果我们想要把自然语言的`^`符号换成Python的`**`,我们可以直接修改原有的ast: 67 | 68 | ![IMAGE](resources/C2DA06C0F8A4D40C55ADF4BB8D3312B4.jpg) 69 | 70 | 当目标代码和原有代码差别较大时,可以直接遍历原有ast生成一颗新的ast。 71 | 72 | > 代码生成器 73 | 74 | 通过递归遍历AST,生成目标代码。以上图较为简单的ast为例,可以将其转换成多种目标语言: 75 | 76 | Python 77 | 78 | ```python 79 | 12 * (3 + 4) ** 2 80 | ``` 81 | 82 | Racket 83 | 84 | ```lisp 85 | #lang racket 86 | 87 | ; 提前声明**函数 88 | (define ** 89 | (lambda (a b) 90 | (expt a b))) 91 | 92 | ; 遍历AST生成的代码 93 | (* 12 94 | (** (+ 3 95 | 4) 96 | 2 97 | ``` 98 | 99 | 当然,以上只是简述,各部分具体的实现都有很多值得研究的地方。 100 | 101 | ## 本项目整体转译流程 102 | 103 | - 根据输入的列表,读取所有文件 104 | - 调用VUE的HTML Parser,解析输入的标签及属性,生成一颗DOM树 105 | - 在解析组件的起始标签时,对其上包含的属性值进行解析(边Parse边做Transform) 106 | - 根据已有的AST生成JS文件(Generate) 107 | 108 | ## 顶部部分:直接复制 109 | 110 | 顶部定义的代码提供给尾部的代码使用,这部分代码直接复制进目标文件即可: 111 | 112 | ```js 113 | export function genTemplate(slot) { 114 | return ` 115 | // 顶部定义的各种函数 116 | function _a () {} 117 | function _b () {} 118 | function _c () {} 119 | 120 | // 中部和尾部代码 121 | ${slot} 122 | ` 123 | } 124 | ``` 125 | 126 | ## 中间部分:wxml属性的处理 127 | 128 | 该部分的作用是收集所有文件内所有组件上的所有属性的值, 尾部代码将会使用这些属性。引用时首先找出所需属性在数组中所出现的位置,然后将其作为参数传递给节点生产函数。 129 | 130 | 小程序支持在组件属性中绑定动态值,如下面代码中view组件的hidden属性值是动态生成的,这个值包裹在双花括号中: 131 | 132 | ```html 133 | 134 | ``` 135 | 136 | 双花括号中的代码为JS语言,VUE内置的HTML Parser不能处理该语言,所以此处需要引入一个JS Parser来解析花括号中的属性,本项目中使用的解析器为:[babylon](https://github.com/babel/babylon) 137 | 138 | 从文章开头的示例,你可以看到,组件的属性值会被解析成数组的形式,这是我们的目标语言,而输入的语言则为JS。首先我们使用JS解析器将JS语言转换为相应的AST: 139 | 140 | ```js 141 | === 输入 JS=== 142 | 3 + 4 143 | 144 | === 输出 AST=== 145 | { 146 | "type": "ExpressionStatement", 147 | "expression": { 148 | "type": "BinaryExpression", 149 | "left": { 150 | "type": "Literal", 151 | "value": 3, 152 | }, 153 | "operator": "+", 154 | "right": { 155 | "type": "Literal", 156 | "value": 4, 157 | } 158 | } 159 | } 160 | 161 | === 目标 数组形式 === 162 | [[2, "+"], [1, 3], [1, 4]] 163 | ``` 164 | 165 | 从目标语言可以推测出,BinaryExpression 可以解析为如下形式: 166 | 167 | ```js 168 | `[[2 "${operator}"], ${left}, ${right}]` 169 | ``` 170 | 171 | Literal 则解析为如下形式: 172 | 173 | ```js 174 | `[[1], ${value}]` 175 | ``` 176 | 177 | 使用JS代码实现的Code Generator如下([点我在线运行](https://babeljs.io/repl/#?babili=false&evaluate=true&lineWrap=true&presets=stage-3&targets=&browsers=&builtIns=false&debug=false&code_lz=MYewdgzgLgBAhtGBeGBvAUDLMBEUCeADgKY4BcuAogB6EBOxEEAluAMpRxTEC2xYUHABpM2HMVoMmrMOTSjsYgiTk4AQszBw6-GvUYtwwhYtwAbYgDNBFDKfu5lpCjgAyzbnThnjD0zgA3bwBXZxgAZhNFAF8RPxwQEi8oEDpVAGpfBxw6ZgBzAAsbeT9FPCIwtw9iLx840qxAkLCAFijsaJNOzvRLYLBgKBkYAHdvAGsYAAowEAATYgBKEpgIEY9gAunZhYA6J2W7bGAEYhgAcg0tHT0pQzBzsnaGKGC6MBgAAwBtb4AmIS4AAkqB2xF2iRqXFS0RwAF1ASCxmZxjN5uCLNZFrEYEiJmi9rlClBsXDPu0AEYMODjEwnCBnc7uTzeR7PYivd5fb4ARkRoPRuyCZlC0TJlOptKw3XQ6FAkFgoAWyFG-IQUF2En00nAizl4AgIAsuzMIDyUyVSyAA&prettier=false&showSidebar=true)): 178 | 179 | ```js 180 | function walk (node) { 181 | switch (node.type) { 182 | case 'BinaryExpression': 183 | return `[[2, "${node.operator}"], ${walk(node.left)}, ${walk(node.right)}]` 184 | break 185 | case 'Literal': 186 | return `[1, ${node.value}]` 187 | break 188 | } 189 | } 190 | 191 | const code = walk(ast.expression) 192 | ``` 193 | 194 | 转换后的属性值会被PUSH到数组z中,以供尾部的代码使用,为了方便的找寻其在数组中的位置,我们需要用到哈希表来记录属性值与其解析后的表达式之间的关系: 195 | 196 | ```js 197 | === 输入 HTML=== 198 | 199 | 200 | === 存放数据的结构 === 201 | { 202 | // 简析前后的映射(key为属性值,value为属性值在属性值列表中的位置) 203 | map: { 'addNumberToFront': 0, ' Add to the front ': 1 }, 204 | // 解析后的数据 205 | props: [ '[3, \'addNumberToFront\']', '[3, \' Add to the front \']' ] 206 | } 207 | ``` 208 | 209 | ## 结尾部分:wxml标签的处理 210 | 211 | 尾部标签的处理与上面类似,先使用HTML Parser将HTML标签转换为一颗AST,再遍历AST来生成JS代码([点我在线运行](https://babeljs.io/repl/#?babili=false&evaluate=true&lineWrap=true&presets=stage-3&targets=&browsers=&builtIns=false&debug=false&code_lz=MYewdgzgLgBAhtGBeGBvAUDGUCeAHAUwC4YByKOAc1IBpMYw4BbYsgIwFcopxb65uAJwCWbCCQDaqGAGsCOEqTbCwAEwp5aMAG5wANh1ak4q1QDkOTNgUEAVEADFB4KKRgBfALp0swABbCeqqCBGCS9FgYWNEwqgJwijAAgqbYINh-BDAAZs5gsHwxWLiEilAEAB6uER70nuju6OigkLB4zngAsnB4yGj0TD0k0ibmltZ2jnlQJAAMNGTJqTwZWbkuZCQAjB4-MO0geOIwEgBEEgDMC8amFlY29k4upJ6nC-dXiymqaas502RXvVGuhshwwMAoMJwDBKKEbAICGZmFkABQASn6WBCUA4gjAZAA-hU3ABqGDdKB-AB0gjgahATAx1J4AGUoCIwJRURcAGzo6kQDhiDmogBMCwu6IaTTBEKhMLhYFsBCYeD0iJgqIORwWED0ICgmKiMBaiB13Tw7MEfR1EGpgzw2o6yAAfDAAAYAL2peA4ED8qIAJKgde50R6BQArEAqVGkAA6YFI0uxBFx-M9NV0Nq9fQk9RiIYtPWtjRiNTlkOhBMJAgo_iSQlRYBAqgIC3rgmRLAWeBAEGNNSwrfb1K7Ei7PYInj6oDVXAIADV9IZUV6JP2IJ5U9Fy9FK-DqzD537yiuDARUbpL0OijAIAB3YRQfzX1cECSzHdY--mhBZKQFykEQw5FDieIEjehgSFshZFPuWCITANQhvqhr7h6MqgkeCoEo--gyFqo4dvscAhPk053qa4CIFW059EqCLlNOGL0E-L7-MRbYECy-AENRvgAWQFDUKB95mrAXYOEefQkeOQiiBAYFYA6PSoip0Rdm6mkxB6dbcHAjbNiG9Eou4NCnCGXbUnIODuG8xYdJaal4JOQjUtBM7hlhf4wLu97UjGcaJsmAXRBBma-X-OYwKZR7Tu4fSEsAISIrYVColZqDyYwLAOeFRaoNJR7IUV8n-IEwShK5qKVUEbowARegyHVARBAsZksOi0axmA8YpmV0SEn6AYAMLtaowahuRoRQIlMA0PFEKJYVWYxMAwnkJUrjieB6aQetMXkXFqBdQQSUoClaXlCqVRmDx00lngrkSPJcQUJ44aaSN_p-BNVVPbNlHmYty3AKtYHRY0IKSTR7Z9M1rUIFA1zOIaKbNLRIB6LxBrckqKpqhq5TOocLl2gsoDtj16BAA&prettier=false&showSidebar=true&#%3Fbabili=false&%23?babili=false)) 212 | 213 | ```js 214 | === 输入 HTML=== 215 | 216 | 217 | === 输出 AST=== 218 | const ast = { 219 | type: 'tag', 220 | name: 'button', 221 | attribs: [{ key: 'bindtap', value: 'addNumberToFront' }], 222 | children: [ 223 | { 224 | data: ' Add to the front ', 225 | type: 'text' 226 | } 227 | ] 228 | } 229 | 230 | === 目标 JS代码 === 231 | var A = _createTag("button") 232 | _attachAttr(A,"bindtap",0) 233 | var A = _createTextNode(1) 234 | _pushChild(A ,A) 235 | _pushChild(root ,A)) 236 | ``` 237 | -------------------------------------------------------------------------------- /docs/resources/C2DA06C0F8A4D40C55ADF4BB8D3312B4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IOriens/wxml-transpiler/142e99fb497339fce0548c1fb28f72825513640f/docs/resources/C2DA06C0F8A4D40C55ADF4BB8D3312B4.jpg -------------------------------------------------------------------------------- /docs/resources/D26E6CDB0A8FD3838D3BB09ECF22727C.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IOriens/wxml-transpiler/142e99fb497339fce0548c1fb28f72825513640f/docs/resources/D26E6CDB0A8FD3838D3BB09ECF22727C.jpg -------------------------------------------------------------------------------- /flow/compiler.js: -------------------------------------------------------------------------------- 1 | declare type CompilerOptions = { 2 | warn?: Function; // allow customizing warning in different environments; e.g. node 3 | expectHTML?: boolean; // only false for non-web builds 4 | modules?: Array; // platform specific modules; e.g. style; class 5 | staticKeys?: string; // a list of AST properties to be considered static; for optimization 6 | directives?: { [key: string]: Function }; // platform specific directives 7 | isUnaryTag?: (tag: string) => ?boolean; // check if a tag is unary for the platform 8 | canBeLeftOpenTag?: (tag: string) => ?boolean; // check if a tag can be left opened 9 | isReservedTag?: (tag: string) => ?boolean; // check if a tag is a native for the platform 10 | mustUseProp?: (tag: string, type: ?string, name: string) => boolean; // check if an attribute should be bound as a property 11 | isPreTag?: (attr: string) => ?boolean; // check if a tag needs to preserve whitespace 12 | getTagNamespace?: (tag: string) => ?string; // check the namespace for a tag 13 | transforms?: Array; // a list of transforms on parsed AST before codegen 14 | preserveWhitespace?: boolean; 15 | isFromDOM?: boolean; 16 | shouldDecodeTags?: boolean; 17 | shouldDecodeNewlines?: boolean; 18 | 19 | // for ssr optimization compiler 20 | scopeId?: string; 21 | 22 | // runtime user-configurable 23 | delimiters?: [string, string]; // template delimiters 24 | 25 | // allow user kept comments 26 | comments?: boolean 27 | }; 28 | 29 | declare type CompiledResult = { 30 | ast: ?ASTElement; 31 | render: string; 32 | staticRenderFns: Array; 33 | stringRenderFns?: Array; 34 | errors?: Array; 35 | tips?: Array; 36 | tags?: Array; 37 | }; 38 | 39 | declare type ModuleOptions = { 40 | preTransformNode: (el: ASTElement) => void; 41 | transformNode: (el: ASTElement) => void; // transform an element's AST node 42 | postTransformNode: (el: ASTElement) => void; 43 | genData: (el: ASTElement) => string; // generate extra data string for an element 44 | transformCode?: (el: ASTElement, code: string) => string; // further transform generated code for an element 45 | staticKeys?: Array; // AST properties to be considered static 46 | }; 47 | 48 | declare type ASTModifiers = { [key: string]: boolean }; 49 | declare type ASTIfConditions = Array<{ exp: ?string; block: ASTElement }>; 50 | 51 | declare type ASTElementHandler = { 52 | value: string; 53 | modifiers: ?ASTModifiers; 54 | }; 55 | 56 | declare type ASTElementHandlers = { 57 | [key: string]: ASTElementHandler | Array; 58 | }; 59 | 60 | declare type ASTDirective = { 61 | name: string; 62 | rawName: string; 63 | value: string; 64 | arg: ?string; 65 | modifiers: ?ASTModifiers; 66 | }; 67 | 68 | declare type ASTNode = ASTElement | ASTText | ASTExpression; 69 | 70 | declare type ASTElement = { 71 | type: 1; 72 | tag: string; 73 | attributeList: Array<{ name: string; value: string }>; 74 | attributeMap: { [key: string]: string | null }; 75 | parent: ASTElement | void; 76 | children: Array; 77 | nodeFuncName?: string; 78 | 79 | env?:string; 80 | scope?:string; 81 | 82 | static?: boolean; 83 | staticRoot?: boolean; 84 | staticInFor?: boolean; 85 | staticProcessed?: boolean; 86 | hasBindings?: boolean; 87 | 88 | text?: string; 89 | attrs?: Array<{ name: string; value: string }>; 90 | props?: Array<{ name: string; value: string }>; 91 | plain?: boolean; 92 | pre?: true; 93 | ns?: string; 94 | 95 | component?: string; 96 | inlineTemplate?: true; 97 | transitionMode?: string | null; 98 | slotName?: ?string; 99 | slotTarget?: ?string; 100 | slotScope?: ?string; 101 | scopedSlots?: { [name: string]: ASTElement }; 102 | 103 | ref?: string; 104 | refInFor?: boolean; 105 | 106 | if?: string; 107 | ifProcessed?: boolean; 108 | elseif?: string; 109 | else?: true; 110 | ifConditions?: ASTIfConditions; 111 | 112 | for?: string; 113 | forProcessed?: boolean; 114 | key?: string; 115 | alias?: string; 116 | iterator1?: string; 117 | iterator2?: string; 118 | 119 | staticClass?: string; 120 | classBinding?: string; 121 | staticStyle?: string; 122 | styleBinding?: string; 123 | events?: ASTElementHandlers; 124 | nativeEvents?: ASTElementHandlers; 125 | 126 | transition?: string | true; 127 | transitionOnAppear?: boolean; 128 | 129 | model?: { 130 | value: string; 131 | callback: string; 132 | expression: string; 133 | }; 134 | 135 | directives?: Array; 136 | 137 | include?: string; 138 | 139 | import?: string; 140 | 141 | blockFuncName?: string; 142 | 143 | importFuncName?: string; 144 | 145 | data?:string; 146 | forbidden?: true; 147 | once?: true; 148 | onceProcessed?: boolean; 149 | wrapData?: (code: string) => string; 150 | wrapListeners?: (code: string) => string; 151 | 152 | // 2.4 ssr optimization 153 | ssrOptimizability?: number; 154 | 155 | // template name 156 | name?: string; 157 | 158 | tmplProcessed?: bool; 159 | 160 | // weex specific 161 | appendAsTree?: boolean; 162 | }; 163 | 164 | declare type ASTExpression = { 165 | type: 2; 166 | expression: string; 167 | text: string; 168 | static?: boolean; 169 | // 2.4 ssr optimization 170 | ssrOptimizability?: number; 171 | }; 172 | 173 | declare type ASTText = { 174 | type: 3; 175 | text: string; 176 | static?: boolean; 177 | isComment?: boolean; 178 | // 2.4 ssr optimization 179 | ssrOptimizability?: number; 180 | }; 181 | 182 | // SFC-parser related declarations 183 | 184 | // an object format describing a single-file component. 185 | declare type SFCDescriptor = { 186 | template: ?SFCBlock; 187 | script: ?SFCBlock; 188 | styles: Array; 189 | customBlocks: Array; 190 | }; 191 | 192 | declare type SFCCustomBlock = { 193 | type: string; 194 | content: string; 195 | start?: number; 196 | end?: number; 197 | src?: string; 198 | attrs: {[attribute:string]: string}; 199 | }; 200 | 201 | declare type SFCBlock = { 202 | type: string; 203 | content: string; 204 | start?: number; 205 | end?: number; 206 | lang?: string; 207 | src?: string; 208 | scoped?: boolean; 209 | module?: string | boolean; 210 | }; 211 | 212 | 213 | declare type Store = { 214 | codeInfoMap:Array; 215 | map: Object; 216 | props: Array; 217 | tags: Array; 218 | }; 219 | 220 | declare type TemplateInfo = { 221 | path: string; 222 | ti: Array; 223 | ic: Array; 224 | templates: Array