UNPKG

@alicd/templateparser

Version:

walle template parser

496 lines (444 loc) 14.4 kB
/** * @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, '&lt;') .replace(/>/g, '&gt;') .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;