未验证 提交 8483ee0b 编写于 作者: B beatles-chameleon 提交者: GitHub

Merge pull request #162 from didi/0.4.x-dev-mvvm-wmj

0.4.x dev mvvm wmj
......@@ -9,23 +9,21 @@ const _ = module.exports = {};
1 :id="value" => v-bind:id="value"
2 @click="handleClick" => c-bind:click="handleClick" 或者c-catch
*/
_.vueToCml = function(source,options = {}) {
_.trim = function (value) {
return value.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
};
_.vueToCml = function(source, options = {}) {
// 去掉模板中的注释
source = _.preDisappearAnnotation(source);
// 模板中所有的 :id="value" ==> v-bind:id="value"
source = _.preParseBindAttr(source);
// 模板中所有的 @click="handleClick" => c-bind:click="handleClick"
source = _.preParseVueEvent(source);
// 模板通过 @babel/parser进行解析
source = _.preParseTemplate(source);
source = _.compileTemplate(source, options);
// 后置处理:用于处理 \u ,便于解析unicode 中文
source = _.postParseUnicode(source);
source = _.postParseUnicode(source);
if (/;$/.test(source)) { // 这里有个坑,jsx解析语法的时候,默认解析的是js语法,所以会在最后多了一个 ; 字符串;但是在 html中 ; 是无法解析的;
source = source.slice(0, -1);
}
return {
source,
usedBuildInTagMap: options.usedBuildInTagMap || {}
......@@ -39,6 +37,156 @@ _.preDisappearAnnotation = function (content) {
return '';
})
}
// 解析属性上的 : 以及 @ v-on这样的语法;
_.preParseTemplate = function(source) {
let callbacks = {startCallback: _.startCallback};
let htmlArr = _.preParseHTMLtoArray(source, callbacks);
let newHtmlArr = [];
htmlArr.forEach((item) => {
if (item.type === 'tagContent') { // 标签内置直接push内容
newHtmlArr.push(item.content);
}
if (item.type === 'tagEnd') {
newHtmlArr.push(item.content);
}
if (item.type === 'tagStart') {
newHtmlArr.push(item.content)
}
});
return newHtmlArr.join('')
}
_.preParseHTMLtoArray = function(html, callbacks) {
let {startCallback} = callbacks;
// 需要考虑问题 单标签和双标签
let stack = [];
// id="value" id='value' class=red disabled
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// 标签的匹配,这些正则都不是g这种全局匹配,所以仅仅会匹配第一个遇到的标签;
// const startTag = new RegExp(`^<${qnameCapture}([\\s\\S])*(\\/?)>`);
// const startTag = /^<([a-zA-Z-:.]*)[^>]*?>/;
const startTagOpen = new RegExp(`^<${qnameCapture}`) // 匹配开始open
const startTagClose = /^\s*(\/?)>/ // 匹配开始关闭;单标签的关闭有两种情况,第一就是 > 第二个就是 />,可以通过捕获分组 / 来判断是单闭合标签还是双开标签的开始标签的闭合
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
let index = 0;
while (html) {
last = html;
let textEnd = html.indexOf('<')
// 解析标签内容,包括开始标签以及结束标签
if (textEnd === 0) { // 以 < 开头的html
const startTagMatch = parseStartTag();
if (startTagMatch) {
stack.push(startTagMatch);
continue;
}
const endTagMatch = parseEndTag();
if (endTagMatch) {
stack.push(endTagMatch);
continue;
}
}
// 解析标签中间的内容
let text, rest, next
if (textEnd >= 0) {
rest = html.slice(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest)
) {
// < in plain text, be forgiving and treat it as text
next = rest.indexOf('<', 1)
if (next < 0) {break}
textEnd += next
rest = html.slice(textEnd)
}
let matchText = {
type: "tagContent"
};
text = html.substring(0, textEnd)
matchText.content = text;
matchText.isText = true;
stack.push(matchText);
advance(textEnd);
continue;
}
if (textEnd < 0) {
text = html;
html = '';
const matchText2 = {
type: 'tagContent',
content: text
}
stack.push(matchText2)
continue;
}
}
return stack;
function advance (n) {
index += n
html = html.substring(n)
}
function parseStartTag () {
// 开始标签也可能是一元标签 通过 isunary 字段进行区分
const start = html.match(startTagOpen)
if (start) {
const matchStart = {
type: 'tagStart',
tagName: start[1],
attrs: []
}
advance(start[0].length);
let end, attr
// 这里处理标签的属性值;
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
matchStart.attrs.push(attr)
}
if (end) {
matchStart.isunary = !!_.trim(end[1] || '');// 标记是否是一元标签
advance(end[0].length)
let attrString = startCallback(matchStart) || '';
let content ;
if (matchStart.isunary) {
content = `<${matchStart.tagName} ${attrString} />`
} else {
content = `<${matchStart.tagName} ${attrString} >`
}
matchStart.content = content;// 每个数组中这个值用于拼接;
return matchStart
}
}
}
function parseEndTag() {
const end = html.match(endTag);
if (end) {
const matchEnd = {
type: 'tagEnd',
tagName: end[1],
content: end[0]
}
advance(end[0].length)
return matchEnd;
}
}
}
_.startCallback = function(matchStart) {
let leftAttrsOnComponent = matchStart.attrs;// 遗留在组件上的属性,默认值是所有属性,
let attrString = (leftAttrsOnComponent || []).reduce((result, item) => {
if (item[1].indexOf(':') === 0) {
item[0] = _.preParseBindAttr(item[0]);// :id="value" ==> v-bind:id="value"
}
if (item[1].indexOf('@') === 0 || item[1].indexOf('v-on') === 0) {
item[0] = _.preParseVueEvent(item[0]);// @click="handleClick" v-on:click="handleClick" ==> c-bind:click="handleClick"
}
result = result + (item[0] || '');
return result;
}, '')
return attrString;
}
// 模板前置处理器
// 预处理:属性 :name="sth" ==> v-bind:name="sth",因为jsx识别不了 :name="sth"
_.preParseBindAttr = function (content) {
......@@ -69,21 +217,21 @@ _.preParseVueEvent = function (content) {
});
return content;
}
_.compileTemplate = function(source,options) {
_.compileTemplate = function(source, options) {
const ast = parser.parse(source, {
plugins: ['jsx']
});
traverse(ast, {
enter(path) {
//所有的入口都以JSXElement为入口解析;
_.parseAllAttributes(path,options);
_.parseBuildTag(path,options);
// 所有的入口都以JSXElement为入口解析;
_.parseAllAttributes(path, options);
_.parseBuildTag(path, options);
}
});
return generate(ast).code;
}
_.isOriginTagOrNativeComp = function(tagName,options){
_.isOriginTagOrNativeComp = function(tagName, options) {
let usedComponentInfo = (options.usingComponents || []).find((item) => item.tagName === tagName)
let isNative = usedComponentInfo && usedComponentInfo.isNative;
let isOrigin = (tagName && typeof tagName === 'string' && tagName.indexOf('origin-') === 0);
......@@ -92,6 +240,7 @@ _.isOriginTagOrNativeComp = function(tagName,options){
}
return false;
}
/*
以标签为基础,解析attruibutes即可
1 v-bind:id="value" ==> id="{{value}}"
......@@ -101,12 +250,12 @@ _.isOriginTagOrNativeComp = function(tagName,options){
*/
_.parseAllAttributes = function(path,options) {
_.parseAllAttributes = function(path, options) {
let node = path.node;
if (t.isJSXElement(node)) {
let tagName = node.openingElement.name.name
if(_.isOriginTagOrNativeComp(tagName,options)){
return //原生标签和原生组件直接不解析
if (_.isOriginTagOrNativeComp(tagName, options)) {
return // 原生标签和原生组件直接不解析
}
let attributes = node.openingElement.attributes;
let directives = ['v-if', 'v-else-if', 'v-else', 'v-model', 'v-show', 'v-text', 'v-for'];
......@@ -229,4 +378,3 @@ _.postParseUnicode = function(content) {
let reg = /\\u/g;
return unescape(content.replace(reg, '%u'));
}
......@@ -4,8 +4,10 @@
"description": "",
"main": "index.js",
"scripts": {
"cover": "istanbul cover --report lcov _mocha -- -R spec --recursive",
"test": "mocha --recursive --reporter spec"
"eslint": "eslint ./lib",
"lint": "eslint --ext .js lib --fix",
"test": "mocha --recursive --reporter spec",
"cover": "istanbul cover --report lcov node_modules/mocha/bin/_mocha -- -R spec --recursive"
},
"author": "Chameleon-Team",
"license": "Apache",
......@@ -15,5 +17,12 @@
"mvvm-babel-generator": "0.4.0-mvvm.6",
"mvvm-babel-parser": "0.4.0-mvvm.6"
},
"devDependencies": {
"chai": "^4.2.0",
"coveralls": "^2.11.9",
"eslint": "^5.9.0",
"istanbul": "^0.4.5",
"mocha": "^5.2.0"
},
"gitHead": "e697386b2323bf287b18774ff482b2c7970f40d8"
}
\ No newline at end of file
}
// const {standardParser, generator, types, traverse} = require('../index.js');
// const template = `
// <view c-bind:tap="click1">
// <text>{{'名称'+name}}</text>
// </view>
// `
// const {ast} = standardParser({
// source: template,
// lang: 'cml'
// })
// traverse(ast, {
// enter: (path) => {
// let node = path.node;
// if (types.isJSXOpeningElement(node)) {
// if (types.isJSXIdentifier(node.name) && node.name.name === 'view') {
// node.name.name = 'div';
// }
// }
// if (types.isJSXClosingElement(node)) {
// if (types.isJSXIdentifier(node.name) && node.name.name === 'view') {
// node.name.name = 'div';
// }
// }
// }
// })
// const output = generator(ast);
// console.log(output)
const expect = require('chai').expect;
const processTemplate = require('../lib/process-template.js');
let options = {
lang: 'cml',
buildInComponents: {button: "cml-buildin-button"},
usingComponents: [{
tagName: 'third-comp1',
refUrl: '/path/to/ref1',
filePath: 'path/to/real1',
isNative: true
}, {
tagName: 'thirdComp2',
refUrl: '/path/to/ref2',
filePath: 'path/to/real2',
isNative: false
}]
};
describe('process-template', function() {
describe('trim', function() {
it('test-trim', function() {
expect(processTemplate.trim(' value ')).to.equal('value');
});
});
describe('vueToCml', function() {
it('test-vueToCml-event', function() {
let source = `<view><!-- 事件 --><view :id="value" name="nameStr" @click="handleClick"></view><view @click.stop="handleClick(1,2,item)"></view><view v-on:click.stop="handleClick(1,2,item)"></view></view>`
expect(processTemplate.vueToCml(source).source).to.equal('<view><view id="{{value}}" name="nameStr" c-bind:tap="handleClick"></view><view c-catch:tap="handleClick(1,2,item)"></view><view c-catch:tap="handleClick(1,2,item)"></view></view>');
});
});
describe('vueToCml', function() {
it('test-vueToCml-directive', function() {
let source = `<view><!-- 指令 v-if v-else-if v-else v-model v-show v-text--><view v-if="1 > 0.5">A</view><view v-else-if="show">B</view><view v-else>c</view><view v-show="!show"></view><view v-model="modelValue"></view><view v-text="text"></view></view>`
expect(processTemplate.vueToCml(source).source).to.equal('<view><view c-if="{{1 > 0.5}}">A</view><view c-else-if="{{show}}">B</view><view c-else>c</view><view c-show="{{!show}}"></view><view c-model="{{modelValue}}"></view><view c-text="{{text}}"></view></view>');
});
});
describe('vueToCml', function() {
it('test-vueToCml-interation-haskey', function() {
let source = `<view><!-- 循环 --><view v-for="item in array" :key="item.id"><view>{{item.id}}{{item[11]}}</view></view></view>`
expect(processTemplate.vueToCml(source).source).to.equal('<view><view c-for="{{array}}" c-for-item="item" c-for-index="index" c-key="id"><view>{{item.id}}{{item[11]}}</view></view></view>');
});
});
describe('vueToCml', function() {
it('test-vueToCml-interation-key *this*', function() {
let source = `<view><!-- 循环 --><view v-for="item in array" :key="item"><view>{{item.id}}{{item[11]}}</view></view></view>`
expect(processTemplate.vueToCml(source).source).to.equal('<view><view c-for="{{array}}" c-for-item="item" c-for-index="index" c-key="*this"><view>{{item.id}}{{item[11]}}</view></view></view>');
});
});
describe('vueToCml', function() {
it('test-vueToCml-interation-nokey', function() {
let source = `<view><!-- 循环 --><view v-for="item in array" ><view>{{item.id}}{{item[11]}}</view></view></view>`
expect(processTemplate.vueToCml(source).source).to.equal('<view><view c-for="{{array}}" c-for-item="item" c-for-index="index"><view>{{item.id}}{{item[11]}}</view></view></view>');
});
});
describe('vueToCml', function() {
it('test-vueToCml-class', function() {
let source = `<view><view class="cls1 cls2" :class="true ?'cls3':'cls4'"></view><view class="cls1 cls2"></view><view :class="true ? 'cls4' : 'cls5'"></view></view>`
expect(processTemplate.vueToCml(source).source).to.equal(`<view><view class=" cls1 cls2 {{true ?'cls3':'cls4'}}"></view><view class=" cls1 cls2"></view><view class=" {{true ? 'cls4' : 'cls5'}}"></view></view>`);
});
});
describe('vueToCml', function() {
it('test-vueToCml-style', function() {
let source = `<view><view style="background-color:red"></view><view :style="computedStyle"></view></view>`
expect(processTemplate.vueToCml(source).source).to.equal(`<view><view style="background-color:red"></view><view style="{{computedStyle}}"></view></view>`);
});
});
// 原生组件和 origin-tag标签
describe('vueToCml', function() {
it('test-vueToCml-origin-tag', function() {
let source = `<view><button></button><third-comp1 v-on:click="click1" :class="true ? 'cls1':'cls2'"></third-comp1><third-comp2 v-on:click="click1" :class="true ? 'cls1':'cls2'"></third-comp2><origin-checkbox v-on:click="click1" :name="13" :class="clas2" class="cls1"></origin-checkbox></view>`
expect(processTemplate.vueToCml(source, options).source).to.equal(`<view><cml-buildin-button></cml-buildin-button><third-comp1 c-bind:tap="click1" v-bind:class="true ? 'cls1':'cls2'"></third-comp1><third-comp2 class=" {{true ? 'cls1':'cls2'}}" c-bind:tap="click1"></third-comp2><origin-checkbox c-bind:tap="click1" v-bind:name="13" v-bind:class="clas2" class="cls1"></origin-checkbox></view>`);
});
});
// 一元标签
describe('vueToCml', function() {
it('test-vueToCml-isunary', function() {
let source = `<view><input :id="value" v-bind:id="value" @click="handleClick" v-on:click="handleClick" /><input style="width:100px" class="cls1" :class="true ? 'cls2':'cls3'" /></view>`
expect(processTemplate.vueToCml(source).source).to.equal(`<view><input id="{{value}}" id="{{value}}" c-bind:tap="handleClick" c-bind:tap="handleClick" /><input class=" cls1 {{true ? 'cls2':'cls3'}}" style="width:100px" /></view>`);
});
});
// 单属性
describe('vueToCml', function() {
it('test-vueToCml-singleAttr', function() {
let source = `<div disabled><input disabled :id="value" v-bind:id="value" @click="handleClick" v-on:click="handleClick" /><input style="width:100px" class="cls1" :class="true ? 'cls2':'cls3'" /></div>`
expect(processTemplate.vueToCml(source).source).to.equal(`<div disabled><input disabled id="{{value}}" id="{{value}}" c-bind:tap="handleClick" c-bind:tap="handleClick" /><input class=" cls1 {{true ? 'cls2':'cls3'}}" style="width:100px" /></div>`);
});
});
// 汉字
describe('vueToCml', function() {
it('test-vueToCml-isunary', function() {
let source = `<div disabled><p title="滴滴">{{出行}}</p></div>`
expect(processTemplate.vueToCml(source).source).to.equal(`<div disabled><p title="滴滴">{{出行}}</p></div>`);
});
});
describe('preDisappearAnnotation', function() {
it('test-preDisappearAnnotation', function() {
let source = `<div class="hello"><!-- <div @click="handleClick">{{numStr}}</div> --></div>`
expect(processTemplate.preDisappearAnnotation(source)).to.equal(`<div class="hello"></div>`);
});
});
describe('preParseHTMLtoArray', function() {
it('test-preParseHTMLtoArray', function() {
let source = `<div class="hello"><div :id="value" v-bind:id="value"></div><div @click="value" v-bind:id="value"></div><input class=" cls1 {{true ? 'cls2':'cls3'}}" style="width:100px" /></div>`
let callbacks = {startCallback: processTemplate.startCallback};
expect(processTemplate.preParseHTMLtoArray(source, callbacks)).to.be.an('array');
});
});
describe('startCallback', function() {
it('test-startCallback', function() {
let matchStart = {
attrs: [
[' :id="value"', ':id'],
[' @click="handleClick"', "@click"],
[' v-on:click="handleClick"', "v-on:click"]
]
}
expect(processTemplate.startCallback(matchStart)).to.be.equal(` v-bind:id="value" c-bind:tap="handleClick" c-bind:tap="handleClick"`);
});
});
describe('preParseBindAttr', function() {
it('test-preParseBindAttr', function() {
let source = ` :id="value"`
expect(processTemplate.preParseBindAttr(source)).to.be.equal(` v-bind:id="value"`);
});
});
describe('preParseVueEvent', function() {
it('test-preParseVueEvent-@', function() {
let source = `@click="handleClick"`
expect(processTemplate.preParseVueEvent(source)).to.be.equal(`c-bind:tap="handleClick"`);
});
});
describe('preParseVueEvent', function() {
it('test-preParseVueEvent-v-on', function() {
let source = `v-on:click="handleClick"`
expect(processTemplate.preParseVueEvent(source)).to.be.equal(`c-bind:tap="handleClick"`);
});
});
describe('isOriginTagOrNativeComp', function() {
it('test-isOriginTagOrNativeComp-isNative', function() {
expect(processTemplate.isOriginTagOrNativeComp('third-comp1', options)).to.be.ok;
});
});
describe('isOriginTagOrNativeComp', function() {
it('test-isOriginTagOrNativeComp-isOriginTag', function() {
expect(processTemplate.isOriginTagOrNativeComp('origin-input', options)).to.be.ok;
});
});
describe('isOriginTagOrNativeComp', function() {
it('test-isOriginTagOrNativeComp-is-not-originTag or native', function() {
expect(processTemplate.isOriginTagOrNativeComp('span', options)).to.be.not.ok;
});
});
describe('analysisFor', function() {
it('transform analysisFor ', function() {
expect(processTemplate.analysisFor(`(item,index) in items`)).to.includes.keys(`item`)
expect(processTemplate.analysisFor(`(item,index) in items`)).to.includes.keys(`index`)
expect(processTemplate.analysisFor(`(item,index) in items`)).to.includes.keys(`list`)
})
});
describe('analysisFor', function() {
it('transform analysisFor ', function() {
expect(processTemplate.analysisFor(`item in items`)).to.includes.keys(`item`)
expect(processTemplate.analysisFor(`item in items`)).to.includes.keys(`index`)
expect(processTemplate.analysisFor(`item in items`)).to.includes.keys(`list`)
})
});
})
const {vueToCml, cmlparse} = require('../index.js');
let source = `<template>
<view>
<!-- 事件 -->
<view :id="value" name="nameStr" @click="handleClick"></view>
<view @click.stop="handleClick(1,2,item)"></view>
<view v-on:click.stop="handleClick(1,2,item)"></view>
<!-- 指令 v-if v-else-if v-else v-model v-show v-text-->
<view v-if="1 > 0.5">A</view>
<view v-else-if="show">B</view>
<view v-else>c</view>
<view v-show="!show"></view>
<view v-model="modelValue"></view>
<view v-text="text"></view>
<!-- 循环 -->
<view v-for="item in array" :key="item.id">
<view>{{item.id}}{{item[11]}}</view>
</view>
<view style="background-color:red"></view>
<view :style="computedStyle"></view>
<view class="cls1 cls2" :class="true ? 'cls3':'cls4'"></view>
<view class="cls1 cls2"></view>
<view :class="true ? 'cls4' : 'cls5'"></view>
<button></button>
<third-comp1 v-on:click="click1"></third-comp1>
<origin-checkbox v-on:click="click1" :name="13" :class="clas2" class="cls1"></origin-checkbox>
</view>
</template>`
let options = {lang: 'cml',
buildInComponents: {button: "cml-buildin-button"},
usingComponents: [{
tagName: 'third-comp1',
refUrl: '/path/to/ref1',
filePath: 'path/to/real1',
isNative: true
}, {
tagName: 'thirdComp2',
refUrl: '/path/to/ref2',
filePath: 'path/to/real2',
isNative: false
}]
};
// let source = `<view style="{{item.id}}">{{item.id}}{{item[11]}}</view>`
let result = vueToCml(source, options);
console.log(result);
let ast = cmlparse(result.source);
// console.log(ast)
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册