@alicd/templateparser
Version:
walle template parser
496 lines (444 loc) • 14.4 kB
JavaScript
/**
* @license
* Copyright Alibaba Group and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import htmlParser from '@alicd/htmlparser';
import config from './config';
import style from './modules/style';
import express from './modules/express';
import _output from './modules/output';
const jsRegExp = new RegExp(
'(\\' + config.expressStartTag + '.+?\\' + config.expressEndTag + '{1,2})'
);
const replaceExpress = new RegExp(
'^\\' + config.expressStartTag + '|\\' + config.expressEndTag + '$',
'g'
);
const forEach = (obj, processCallback) => {
for (const i in obj) {
if (obj.hasOwnProperty(i)) {
processCallback(obj[i], i);
}
}
};
const isEmptyObject = obj => {
let i = 0;
if (typeof obj !== 'object') {
return null;
}
forEach(obj, (item, index) => {
i = index + 1;
});
return !i;
};
/**
* traverse arr to fill holder.
*
* @param {Array} tree array
* @returns array
*/
function traverseArrToFillHolder(tree) {
tree.forEach(treeItem => {
for (let i = 0; i < treeItem.length; i++) {
// holder: 0
if (treeItem[i] === undefined) {
treeItem[i] = 0;
}
}
// handle 'children'
if (Array.isArray(treeItem[3])) {
if (treeItem[3].length) {
traverseArrToFillHolder(treeItem[3]);
} else {
delete treeItem[3];
}
}
});
return tree;
}
class TemplateParser {
constructor(options) {
this.options = options || {};
}
// 需要解析的模板
template = null;
// 模板的解析结果,不含有上下文变量
domJSON = null;
// domJSON的解析结果,含有上下文变量
jsonTree = null;
// 模板注释的正则表达式
commentContentRegExp = /<!--[\w\W\r\n]*?-->/g;
/**
* render template to logic json.
*
* @param {string} template template
* @param {boolean} runtimeOn whether or not use for runtime
* @memberof TemplateParser
*/
render(template, runtimeOn) {
this.template = template;
this.tagStartReplaceRegExp = new RegExp(config.tagStartReplaceStr, 'g');
this.tagEndReplaceRegExp = new RegExp(config.tagEndReplaceStr, 'g');
this.parseTemplate();
this.createJSONTree(runtimeOn);
}
preHandleExpress(str) {
var r = /[\{\}]/g;
var state = 0;
var result = [];
var startIndex = 0;
while (r.test(str)) {
var lastIndex = r.lastIndex;
var matched = str.charAt(lastIndex - 1);
if (matched === '{') {
state++;
if (startIndex === 0) {
startIndex = lastIndex;
}
} else {
state--;
}
if (state === 0) {
let slice = str.slice(0, startIndex);
let replaced = str.slice(startIndex, lastIndex);
replaced = replaced
.replace(/</g, '<')
.replace(/>/g, '>')
.trim();
result.push(slice);
result.push(replaced);
r.lastIndex = 0;
str = str.slice(lastIndex);
startIndex = 0;
}
}
result.push(str);
return result.join('');
}
/**
* 模板解析功能,解析成标准的json树
* @param tpl String/html选择器
*/
parseTemplate(tpl) {
const template = tpl || this.template;
const commentContent = this.commentContentRegExp;
const temEle = template;
const tagStartReplaceStr = config.tagStartReplaceStr;
const tagEndReplaceStr = config.tagEndReplaceStr;
const tagLeft = new RegExp('\\<', 'g');
const tagRight = new RegExp('\\>', 'g');
let temString = null;
if (typeof temEle === 'string') {
temString = temEle.trim().replace(/[\n\r]/g, '');
} else if (
typeof temEle === 'object' &&
temEle.getAttribute('type') === 'text/template'
) {
temString = temEle.innerHTML.replace(/[\n\r]/g, '').trim();
} else {
return false;
}
if (this.options.comment !== true) {
temString = temString.replace(commentContent, '');
}
if (!temString) {
this.domJSON = null;
console.error('!!页面缺少模板代码');
temString = 'null';
}
temString = temString.replace(/(<[\s]+)/g, tagStartReplaceStr + ' ');
let regex = /(\{[^\{\}]*(?:\{[^\{\}]*\}[^\{\{]*)*\})/;
regex = /(\{.+?\})(?=\s+|\/>|>|<|$)/;
let arr = temString.split(regex);
forEach(arr, (item, index) => {
if (!item) {
delete arr[index];
return false;
}
if (express.test(item)) {
arr[index] = item
.replace(tagLeft, tagStartReplaceStr)
.replace(new RegExp('[\\s]?\\>[\\s]?', 'g'), tagEndReplaceStr);
} else if (item.split(/(\{.+?\})/)) {
const splitArr = item.split(/(\{.+?\})/);
forEach(splitArr, (it, i) => {
if (!it) {
splitArr[i] = '';
return false;
}
if (express.test(it)) {
splitArr[i] = it.replace(tagRight, tagEndReplaceStr);
}
});
arr[index] = splitArr.join('');
}
});
temString = this.preHandleExpress(temString);
const tagReg = new RegExp('(<\\/?[\\w]+[^<>]*[\\/]?>)', 'g');
arr = temString.split(tagReg);
forEach(arr, (item, index) => {
// 去除页面标签之间的多余空格
item = item.trim();
if (!item.match(/^</) || !item.match(/>$/)) {
arr[index] = item
.replace(tagLeft, tagStartReplaceStr)
.replace(tagRight, tagEndReplaceStr);
}
});
temString = arr.join('');
arr = null;
if (temString) {
const handler = new htmlParser.DefaultHandler((error, domJSON) => {
if (!error) {
this.domJSON = domJSON;
} else {
console.error(error);
}
});
const parser = new htmlParser.Parser(handler);
const howManyTagsRegexp = new RegExp('(<[\\w]+[^<>]*[\\/]?>)', 'g');
const tags = temString.match(howManyTagsRegexp);
this.tagsNumber = (tags && tags.length) || 0;
// parse complete.
parser.parseComplete(temString);
}
}
/**
* 根据标签名判断是否是组件
* @param tagName 标签名称
* @param componentsRegExp 是否是组件的规则
* @returns {*}
*/
judgmentComponents(tagName, componentsRegExp) {
componentsRegExp = componentsRegExp || this.componentsRegExp;
// 惰性函数
this.judgmentComponents = _tagName => {
const exp = componentsRegExp;
return _tagName.match(exp);
};
return this.judgmentComponents(tagName);
}
/**
* transform dom json to logic json.
* t -> type ( 0 -> text, 1 -> tag, 2 -> component )
* n -> name
* a -> attributes
* c -> children ( array -> children, string/function -> text node )
*
* structure: [t, n, a, c]
* holder: 0
*
* @param domJSON dom json tree
*/
createJSONTree(runtimeOn) {
const domJSON = this.domJSON;
if (!domJSON) {
return false;
}
const returnJSON = [];
const componentsRegExp = config.componentsRegExp;
const loopAttrName = config.loopAttrName;
this.judgmentComponents('div', componentsRegExp);
const isCmp = this.judgmentComponents;
const _loopJSON = (domJSON, recurveJSON, parentAttrs = {}) => {
domJSON.map((dom, _index) => {
// map: 0 -> text, 1 -> tag, 2 -> component
let { type: domType } = dom;
domType = domType === 'text' ? 0 : 1;
// structure: [t, n, a, c]
// mark the components type.
const nodeJSON = [domType];
// dom type: 1 -> tag
if (domType === 1) {
// replace dom data
dom.data = dom.data
.replace(this.tagStartReplaceRegExp, '<')
.replace(this.tagEndReplaceRegExp, '>');
if (isCmp(dom.name)) {
// structure: [t, n, a, c]
nodeJSON[0] = 2;
}
// node tag name.
nodeJSON[1] = dom.name;
// check the attribs
if (dom.attribs && !isEmptyObject(dom.attribs)) {
const attrObj = {};
// replace: class -> className
if (dom.attribs.class) {
dom.attribs.className = dom.attribs.class;
delete dom.attribs.class;
}
// process `x-bind` attr
if (dom.attribs[config.bindAttrName]) {
const _bindPath = express
.filter(dom.attribs[config.bindAttrName])
.trim();
if (config.xbindState.test(_bindPath)) {
dom.attribs['data-bindpath'] = _bindPath.replace(
config.xbindState,
''
);
}
}
// traverse attribs
forEach(dom.attribs, (attrValue, attrName) => {
// handle: boolean
if (typeof attrValue === 'boolean') {
attrObj[attrName] = attrValue;
return;
}
// handle: `...` instruction
if (config.spreadAttribute.test(attrName)) {
attrObj['data-spread'] = _output(attrValue, runtimeOn);
delete attrObj[attrName];
return;
}
// handle: `key` attr
if (attrName === 'key') {
attrObj[attrName] = _output(attrValue, runtimeOn);
return;
}
// trim
attrValue = attrValue.trim();
// handle: style attr
if (
attrName === 'style' ||
attrName === 'inputStyle' ||
attrName === 'overlayStyle'
) {
// format
attrObj[attrName] = style(attrValue, runtimeOn);
return false;
}
// handle: not `x-for` attr
if (attrName !== loopAttrName) {
if (dom.name === 'Action' && attrName === 'handler') {
attrObj[attrName] = attrValue;
} else {
attrObj[attrName] = _output(attrValue, runtimeOn);
}
return false;
}
// handle: `x-for` attr
// structure: [args, loopData]
const forObj = [];
let loopStr;
// walle syntax: with `{}` syntax
if (express.test(attrValue.trim())) {
// replace `{}` to ''
loopStr = attrValue.replace(replaceExpress, '').trim();
}
// handle split arr
const loopStrArr = loopStr.split(' in ');
if (loopStrArr && loopStrArr.length === 2) {
let argumentsStr = loopStrArr[0].trim();
if (argumentsStr.match(/^\([\w\W]*?\)$/)) {
argumentsStr = argumentsStr.replace(/\)$|^\(|\s|\$/g, '');
const argumentsStrArr = argumentsStr.split(',');
if (argumentsStrArr.length >= 2) {
forObj[0] = argumentsStrArr;
} else {
forObj[0] = argumentsStrArr;
}
} else {
forObj[0] = [argumentsStr.replace(/^\$/g, '')];
}
forObj[1] = _output(
config.expressStartTag +
loopStrArr[1].trim() +
config.expressEndTag,
runtimeOn
);
attrObj[attrName] = forObj;
}
});
// get the attrs
// structure: [t, n, a, c]
nodeJSON[2] = attrObj;
}
// check the children
if (dom.children && dom.children.length) {
// case: single text node
if (dom.children.length === 1 && dom.children[0].type === 'text') {
const str = dom.children[0].data.trim();
const children = [];
// walle syntax: with `{}` syntax
if (express.test(str)) {
children.push({
type: 'text',
data: str
});
}
// walle syntax with `{}` partially
else {
const arr = str.split(jsRegExp);
// remove the useless from head to tail
if (!arr[0]) {
arr.splice(0, 1);
}
if (!arr[arr.length - 1]) {
arr.splice(arr.length - 1, 1);
}
// handle the split result
forEach(arr, _it => {
children.push({
type: 'text',
data: _it
});
});
}
// structure: [t, n, a, c]
nodeJSON[3] = [];
_loopJSON(children, nodeJSON[3], parentAttrs);
}
// case: other
else {
nodeJSON[3] = [];
_loopJSON(dom.children, nodeJSON[3], parentAttrs);
}
}
}
// dom type: 0 -> text
// walle syntax: with `{}` syntax
else if (express.test(dom.data)) {
// structure: [t, n, a, c]
nodeJSON[3] = _output(dom.data, runtimeOn);
}
// dom type: 0 -> text
// walle syntax: with `{}` syntax partially
else {
// split the `{}`
const arr = dom.data.split(jsRegExp);
// remove the useless from head to tail
if (!arr[0]) {
arr.splice(0, 1);
}
if (!arr[arr.length - 1]) {
arr.splice(arr.length - 1, 1);
}
// handle the split result
if (arr.length === 1) {
nodeJSON[3] = _output(dom.data, runtimeOn);
} else {
forEach(arr, perText => {
const c = _output(perText, runtimeOn);
const perJSON = [0, 0, 0, c];
recurveJSON.push(perJSON);
});
return false;
}
}
recurveJSON.push(nodeJSON);
return null;
});
};
_loopJSON(domJSON, returnJSON);
// traverse to fill holder with 0
traverseArrToFillHolder(returnJSON);
this.logicJSON = returnJSON;
}
}
export default TemplateParser;